diff options
author | 2024-12-13 17:29:18 -0800 | |
---|---|---|
committer | 2024-12-13 17:29:18 -0800 | |
commit | 7b1f850c50427ff38c20c83721c2e7aa173e62c2 (patch) | |
tree | 92f02a12a16318cde6d9a3b3761ca2c0040ce557 /libs | |
parent | 3f4c4881a31f0a24979df539b9d1e9c557a3619d (diff) | |
parent | 10e260fc86f8b879e9a88610994344f30bea7520 (diff) |
Merge "Merge 24Q4 into AOSP main" into main
Diffstat (limited to 'libs')
1005 files changed, 41400 insertions, 9959 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/AcceptOnceConsumer.java index fe60037483c4..c2f827a22fc2 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/util/AcceptOnceConsumer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/AcceptOnceConsumer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.window.util; +package androidx.window.common; import android.annotation.NonNull; @@ -23,7 +23,7 @@ import java.util.function.Consumer; /** * A base class that works with {@link BaseDataProducer} to add/remove a consumer that should * only be used once when {@link BaseDataProducer#notifyDataChanged} is called. - * @param <T> The type of data this producer returns through {@link DataProducer#getData}. + * @param <T> The type of data this producer returns through {@link BaseDataProducer#getData}. */ public class AcceptOnceConsumer<T> implements Consumer<T> { private final Consumer<T> mCallback; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/BaseDataProducer.java index de52f0969fa8..e7099dc3a281 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/BaseDataProducer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.window.util; +package androidx.window.common; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; @@ -26,13 +26,12 @@ import java.util.Set; import java.util.function.Consumer; /** - * Base class that provides the implementation for the callback mechanism of the - * {@link DataProducer} API. This class is thread safe for adding, removing, and notifying - * consumers. + * Base class that manages listeners when listening to a piece of data that changes. This class is + * thread safe for adding, removing, and notifying consumers. * - * @param <T> The type of data this producer returns through {@link DataProducer#getData}. + * @param <T> The type of data this producer returns through {@link BaseDataProducer#getData}. */ -public abstract class BaseDataProducer<T> implements DataProducer<T>, +public abstract class BaseDataProducer<T> implements AcceptOnceConsumer.AcceptOnceProducerCallback<T> { private final Object mLock = new Object(); @@ -42,12 +41,17 @@ public abstract class BaseDataProducer<T> implements DataProducer<T>, private final Set<Consumer<T>> mCallbacksToRemove = new HashSet<>(); /** + * Emits the first available data at that point in time. + * @param dataConsumer a {@link Consumer} that will receive one value. + */ + public abstract void getData(@NonNull Consumer<T> dataConsumer); + + /** * Adds a callback to the set of callbacks listening for data. Data is delivered through * {@link BaseDataProducer#notifyDataChanged(Object)}. This method is thread safe. Callers * should ensure that callbacks are thread safe. * @param callback that will receive data from the producer. */ - @Override public final void addDataChangedCallback(@NonNull Consumer<T> callback) { synchronized (mLock) { mCallbacks.add(callback); @@ -63,7 +67,6 @@ public abstract class BaseDataProducer<T> implements DataProducer<T>, * @param callback that was registered in * {@link BaseDataProducer#addDataChangedCallback(Consumer)}. */ - @Override public final void removeDataChangedCallback(@NonNull Consumer<T> callback) { synchronized (mLock) { mCallbacks.remove(callback); @@ -92,8 +95,8 @@ public abstract class BaseDataProducer<T> implements DataProducer<T>, /** * Called to notify all registered consumers that the data provided - * by {@link DataProducer#getData} has changed. Calls to this are thread save but callbacks need - * to ensure thread safety. + * by {@link BaseDataProducer#getData} has changed. Calls to this are thread save but callbacks + * need to ensure thread safety. */ protected void notifyDataChanged(T value) { synchronized (mLock) { @@ -122,4 +125,4 @@ public abstract class BaseDataProducer<T> implements DataProducer<T>, mCallbacksToRemove.add(callback); } } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java index 98935e95deaf..37f0067de453 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java @@ -18,8 +18,8 @@ package androidx.window.common; import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER; -import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_UNKNOWN; -import static androidx.window.common.CommonFoldingFeature.parseListFromString; +import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN; +import static androidx.window.common.layout.CommonFoldingFeature.parseListFromString; import android.annotation.NonNull; import android.content.Context; @@ -31,8 +31,8 @@ import android.text.TextUtils; import android.util.Log; import android.util.SparseIntArray; -import androidx.window.util.AcceptOnceConsumer; -import androidx.window.util.BaseDataProducer; +import androidx.window.common.layout.CommonFoldingFeature; +import androidx.window.common.layout.DisplayFoldFeatureCommon; import com.android.internal.R; @@ -44,7 +44,7 @@ import java.util.Optional; import java.util.function.Consumer; /** - * An implementation of {@link androidx.window.util.BaseDataProducer} that returns + * An implementation of {@link BaseDataProducer} that returns * the device's posture by mapping the state returned from {@link DeviceStateManager} to * values provided in the resources' config at {@link R.array#config_device_state_postures}. */ @@ -203,6 +203,23 @@ public final class DeviceStateManagerFoldingFeatureProducer /** + * Returns the list of supported {@link DisplayFoldFeatureCommon} calculated from the + * {@link DeviceStateManagerFoldingFeatureProducer}. + */ + @NonNull + public List<DisplayFoldFeatureCommon> getDisplayFeatures() { + final List<DisplayFoldFeatureCommon> foldFeatures = new ArrayList<>(); + final List<CommonFoldingFeature> folds = getFoldsWithUnknownState(); + + final boolean isHalfOpenedSupported = isHalfOpenedSupported(); + for (CommonFoldingFeature fold : folds) { + foldFeatures.add(DisplayFoldFeatureCommon.create(fold, isHalfOpenedSupported)); + } + return foldFeatures; + } + + + /** * Returns {@code true} if the device supports half-opened mode, {@code false} otherwise. */ public boolean isHalfOpenedSupported() { @@ -214,7 +231,7 @@ public final class DeviceStateManagerFoldingFeatureProducer * @param storeFeaturesConsumer a consumer to collect the data when it is first available. */ @Override - public void getData(Consumer<List<CommonFoldingFeature>> storeFeaturesConsumer) { + public void getData(@NonNull Consumer<List<CommonFoldingFeature>> storeFeaturesConsumer) { mRawFoldSupplier.getData((String displayFeaturesString) -> { if (TextUtils.isEmpty(displayFeaturesString)) { storeFeaturesConsumer.accept(new ArrayList<>()); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/common/ExtensionHelper.java index a08db7939eca..f466d603bda3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/ExtensionHelper.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.window.util; +package androidx.window.common; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java index 8906e6d3d02e..9651918ef5ca 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java @@ -26,7 +26,7 @@ import android.os.Looper; import android.provider.Settings; import android.text.TextUtils; -import androidx.window.util.BaseDataProducer; +import androidx.window.common.layout.CommonFoldingFeature; import com.android.internal.R; @@ -34,7 +34,7 @@ import java.util.Optional; import java.util.function.Consumer; /** - * Implementation of {@link androidx.window.util.DataProducer} that produces a + * Implementation of {@link BaseDataProducer} that produces a * {@link String} that can be parsed to a {@link CommonFoldingFeature}. * {@link RawFoldingFeatureProducer} searches for the value in two places. The first check is in * settings where the {@link String} property is saved with the key diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/collections/ListUtil.java b/libs/WindowManager/Jetpack/src/androidx/window/common/collections/ListUtil.java new file mode 100644 index 000000000000..e72459fe61bf --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/collections/ListUtil.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 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.common.collections; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * A class to contain utility methods for {@link List}. + */ +public final class ListUtil { + + private ListUtil() {} + + /** + * Returns a new {@link List} that is created by applying the {@code transformer} to the + * {@code source} list. + */ + public static <T, U> List<U> map(List<T> source, Function<T, U> transformer) { + final List<U> target = new ArrayList<>(); + for (int i = 0; i < source.size(); i++) { + target.add(transformer.apply(source.get(i))); + } + return target; + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java b/libs/WindowManager/Jetpack/src/androidx/window/common/layout/CommonFoldingFeature.java index e37dea4dfd69..85c4fe1193ee 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/layout/CommonFoldingFeature.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package androidx.window.common; +package androidx.window.common.layout; -import static androidx.window.util.ExtensionHelper.isZero; +import static androidx.window.common.ExtensionHelper.isZero; import android.annotation.IntDef; import android.annotation.Nullable; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/layout/DisplayFoldFeatureCommon.java b/libs/WindowManager/Jetpack/src/androidx/window/common/layout/DisplayFoldFeatureCommon.java new file mode 100644 index 000000000000..594bd9cc3bc6 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/layout/DisplayFoldFeatureCommon.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2024 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.common.layout; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.util.ArraySet; + +import java.util.Objects; +import java.util.Set; + +/** + * A class that represents if a fold is part of the device. + */ +public final class DisplayFoldFeatureCommon { + + /** + * Returns a new instance of {@link DisplayFoldFeatureCommon} based off of + * {@link CommonFoldingFeature} and whether or not half opened is supported. + */ + public static DisplayFoldFeatureCommon create(CommonFoldingFeature foldingFeature, + boolean isHalfOpenedSupported) { + @FoldType + final int foldType; + if (foldingFeature.getType() == CommonFoldingFeature.COMMON_TYPE_HINGE) { + foldType = DISPLAY_FOLD_FEATURE_TYPE_HINGE; + } else { + foldType = DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN; + } + + final Set<Integer> properties = new ArraySet<>(); + + if (isHalfOpenedSupported) { + properties.add(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED); + } + return new DisplayFoldFeatureCommon(foldType, properties); + } + + /** + * The type of fold is unknown. This is here for compatibility reasons if a new type is added, + * and cannot be reported to an incompatible application. + */ + public static final int DISPLAY_FOLD_FEATURE_TYPE_UNKNOWN = 0; + + /** + * The type of fold is a physical hinge separating two display panels. + */ + public static final int DISPLAY_FOLD_FEATURE_TYPE_HINGE = 1; + + /** + * The type of fold is a screen that folds from 0-180. + */ + public static final int DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN = 2; + + /** + * @hide + */ + @IntDef(value = {DISPLAY_FOLD_FEATURE_TYPE_UNKNOWN, DISPLAY_FOLD_FEATURE_TYPE_HINGE, + DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN}) + public @interface FoldType { + } + + /** + * The fold supports the half opened state. + */ + public static final int DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED = 1; + + @IntDef(value = {DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED}) + public @interface FoldProperty { + } + + @FoldType + private final int mType; + + private final Set<Integer> mProperties; + + /** + * Creates an instance of [FoldDisplayFeature]. + * + * @param type the type of fold, either [FoldDisplayFeature.TYPE_HINGE] or + * [FoldDisplayFeature.TYPE_FOLDABLE_SCREEN] + * @hide + */ + public DisplayFoldFeatureCommon(@FoldType int type, @NonNull Set<Integer> properties) { + mType = type; + mProperties = new ArraySet<>(); + assertPropertiesAreValid(properties); + mProperties.addAll(properties); + } + + /** + * Returns the type of fold that is either a hinge or a fold. + */ + @FoldType + public int getType() { + return mType; + } + + /** + * Returns {@code true} if the fold has the given property, {@code false} otherwise. + */ + public boolean hasProperty(@FoldProperty int property) { + return mProperties.contains(property); + } + /** + * Returns {@code true} if the fold has all the given properties, {@code false} otherwise. + */ + public boolean hasProperties(@NonNull @FoldProperty int... properties) { + for (int i = 0; i < properties.length; i++) { + if (!mProperties.contains(properties[i])) { + return false; + } + } + return true; + } + + /** + * Returns a copy of the set of properties. + * @hide + */ + public Set<Integer> getProperties() { + return new ArraySet<>(mProperties); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DisplayFoldFeatureCommon that = (DisplayFoldFeatureCommon) o; + return mType == that.mType && Objects.equals(mProperties, that.mProperties); + } + + @Override + public int hashCode() { + return Objects.hash(mType, mProperties); + } + + @Override + public String toString() { + return "DisplayFoldFeatureCommon{mType=" + mType + ", mProperties=" + mProperties + '}'; + } + + private static void assertPropertiesAreValid(@NonNull Set<Integer> properties) { + for (int property : properties) { + if (!isProperty(property)) { + throw new IllegalArgumentException("Property is not a valid type: " + property); + } + } + } + + private static boolean isProperty(int property) { + if (property == DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED) { + return true; + } + return false; + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java index ecf47209a802..7f11feaa585e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java @@ -39,6 +39,8 @@ import androidx.window.extensions.embedding.SplitController; import androidx.window.extensions.layout.WindowLayoutComponent; import androidx.window.extensions.layout.WindowLayoutComponentImpl; +import com.android.window.flags.Flags; + import java.util.Objects; @@ -55,11 +57,9 @@ class WindowExtensionsImpl implements WindowExtensions { */ private static final int NO_LEVEL_OVERRIDE = -1; - /** - * The min version of the WM Extensions that must be supported in the current platform version. - */ - @VisibleForTesting - static final int EXTENSIONS_VERSION_CURRENT_PLATFORM = 6; + private static final int EXTENSIONS_VERSION_V7 = 7; + + private static final int EXTENSIONS_VERSION_V6 = 6; private final Object mLock = new Object(); private volatile DeviceStateManagerFoldingFeatureProducer mFoldingFeatureProducer; @@ -67,7 +67,6 @@ class WindowExtensionsImpl implements WindowExtensions { private volatile SplitController mSplitController; private volatile WindowAreaComponent mWindowAreaComponent; - private final int mVersion = EXTENSIONS_VERSION_CURRENT_PLATFORM; private final boolean mIsActivityEmbeddingEnabled; WindowExtensionsImpl() { @@ -76,9 +75,22 @@ class WindowExtensionsImpl implements WindowExtensions { Log.i(TAG, generateLogMessage()); } + /** + * The min version of the WM Extensions that must be supported in the current platform version. + */ + @VisibleForTesting + static int getExtensionsVersionCurrentPlatform() { + if (Flags.activityEmbeddingAnimationCustomizationFlag()) { + // Activity Embedding animation customization is the only major feature for v7. + return EXTENSIONS_VERSION_V7; + } else { + return EXTENSIONS_VERSION_V6; + } + } + private String generateLogMessage() { final StringBuilder logBuilder = new StringBuilder("Initializing Window Extensions, " - + "vendor API level=" + mVersion); + + "vendor API level=" + getExtensionsVersionCurrentPlatform()); final int levelOverride = getLevelOverride(); if (levelOverride != NO_LEVEL_OVERRIDE) { logBuilder.append(", override to ").append(levelOverride); @@ -91,7 +103,12 @@ class WindowExtensionsImpl implements WindowExtensions { @Override public int getVendorApiLevel() { final int levelOverride = getLevelOverride(); - return (levelOverride != NO_LEVEL_OVERRIDE) ? levelOverride : mVersion; + return hasLevelOverride() ? levelOverride : getExtensionsVersionCurrentPlatform(); + } + + @VisibleForTesting + boolean hasLevelOverride() { + return getLevelOverride() != NO_LEVEL_OVERRIDE; } private int getLevelOverride() { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java new file mode 100644 index 000000000000..bfccb29bc952 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.embedding; + +import static android.window.TaskFragmentOrganizer.KEY_RESTORE_TASK_FRAGMENTS_INFO; +import static android.window.TaskFragmentOrganizer.KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO; + +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Looper; +import android.os.MessageQueue; +import android.util.ArrayMap; +import android.util.Log; +import android.util.SparseArray; +import android.window.TaskFragmentInfo; +import android.window.TaskFragmentParentInfo; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Helper class to back up and restore the TaskFragmentOrganizer state, in order to resume + * organizing the TaskFragments if the app process is restarted. + */ +@SuppressWarnings("GuardedBy") +class BackupHelper { + private static final String TAG = "BackupHelper"; + private static final boolean DEBUG = Build.isDebuggable(); + + private static final String KEY_TASK_CONTAINERS = "KEY_TASK_CONTAINERS"; + @NonNull + private final SplitController mController; + @NonNull + private final SplitPresenter mPresenter; + @NonNull + private final BackupIdler mBackupIdler = new BackupIdler(); + private boolean mBackupIdlerScheduled; + + private final List<ParcelableTaskContainerData> mParcelableTaskContainerDataList = + new ArrayList<>(); + private final ArrayMap<IBinder, TaskFragmentInfo> mTaskFragmentInfos = new ArrayMap<>(); + private final SparseArray<TaskFragmentParentInfo> mTaskFragmentParentInfos = + new SparseArray<>(); + + BackupHelper(@NonNull SplitController splitController, @NonNull SplitPresenter splitPresenter, + @NonNull Bundle savedState) { + mController = splitController; + mPresenter = splitPresenter; + + if (!savedState.isEmpty()) { + restoreState(savedState); + } + } + + /** + * Schedules a back-up request. It is no-op if there was a request scheduled and not yet + * completed. + */ + void scheduleBackup() { + if (!mBackupIdlerScheduled) { + mBackupIdlerScheduled = true; + Looper.getMainLooper().getQueue().addIdleHandler(mBackupIdler); + } + } + + final class BackupIdler implements MessageQueue.IdleHandler { + @Override + public boolean queueIdle() { + synchronized (mController.mLock) { + mBackupIdlerScheduled = false; + saveState(); + } + return false; + } + } + + private void saveState() { + final List<TaskContainer> taskContainers = mController.getTaskContainers(); + if (taskContainers.isEmpty()) { + Log.w(TAG, "No task-container to back up"); + return; + } + + if (DEBUG) Log.d(TAG, "Start to back up " + taskContainers); + final List<ParcelableTaskContainerData> parcelableTaskContainerDataList = new ArrayList<>( + taskContainers.size()); + for (TaskContainer taskContainer : taskContainers) { + parcelableTaskContainerDataList.add(taskContainer.getParcelableData()); + } + final Bundle state = new Bundle(); + state.setClassLoader(ParcelableTaskContainerData.class.getClassLoader()); + state.putParcelableList(KEY_TASK_CONTAINERS, parcelableTaskContainerDataList); + mController.setSavedState(state); + } + + private void restoreState(@NonNull Bundle savedState) { + if (savedState.isEmpty()) { + return; + } + + if (DEBUG) Log.d(TAG, "Start restoring saved-state"); + mParcelableTaskContainerDataList.addAll(savedState.getParcelableArrayList( + KEY_TASK_CONTAINERS, ParcelableTaskContainerData.class)); + if (DEBUG) Log.d(TAG, "Retrieved tasks : " + mParcelableTaskContainerDataList.size()); + if (mParcelableTaskContainerDataList.isEmpty()) { + return; + } + + final List<TaskFragmentInfo> infos = savedState.getParcelableArrayList( + KEY_RESTORE_TASK_FRAGMENTS_INFO, TaskFragmentInfo.class); + for (TaskFragmentInfo info : infos) { + if (DEBUG) Log.d(TAG, "Retrieved: " + info); + mTaskFragmentInfos.put(info.getFragmentToken(), info); + mPresenter.updateTaskFragmentInfo(info); + } + + final List<TaskFragmentParentInfo> parentInfos = savedState.getParcelableArrayList( + KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO, + TaskFragmentParentInfo.class); + for (TaskFragmentParentInfo info : parentInfos) { + if (DEBUG) Log.d(TAG, "Retrieved: " + info); + mTaskFragmentParentInfos.put(info.getTaskId(), info); + } + } + + boolean hasPendingStateToRestore() { + return !mParcelableTaskContainerDataList.isEmpty(); + } + + /** + * Returns {@code true} if any of the {@link TaskContainer} is restored. + * Otherwise, returns {@code false}. + */ + boolean rebuildTaskContainers(@NonNull WindowContainerTransaction wct, + @NonNull Set<EmbeddingRule> rules) { + if (mParcelableTaskContainerDataList.isEmpty()) { + return false; + } + + if (DEBUG) Log.d(TAG, "Rebuilding TaskContainers."); + final ArrayMap<String, EmbeddingRule> embeddingRuleMap = new ArrayMap<>(); + for (EmbeddingRule rule : rules) { + embeddingRuleMap.put(rule.getTag(), rule); + } + + boolean restoredAny = false; + for (int i = mParcelableTaskContainerDataList.size() - 1; i >= 0; i--) { + final ParcelableTaskContainerData parcelableTaskContainerData = + mParcelableTaskContainerDataList.get(i); + final List<String> tags = parcelableTaskContainerData.getSplitRuleTags(); + if (!embeddingRuleMap.containsAll(tags)) { + // has unknown tag, unable to restore. + if (DEBUG) { + Log.d(TAG, "Rebuilding TaskContainer abort! Unknown Tag. Task#" + + parcelableTaskContainerData.mTaskId); + } + continue; + } + + mParcelableTaskContainerDataList.remove(parcelableTaskContainerData); + final TaskContainer taskContainer = new TaskContainer(parcelableTaskContainerData, + mController, mTaskFragmentInfos); + if (DEBUG) Log.d(TAG, "Created TaskContainer " + taskContainer); + mController.addTaskContainer(taskContainer.getTaskId(), taskContainer); + + for (ParcelableSplitContainerData splitData : + parcelableTaskContainerData.getParcelableSplitContainerDataList()) { + final SplitRule rule = (SplitRule) embeddingRuleMap.get(splitData.mSplitRuleTag); + assert rule != null; + if (mController.getContainer(splitData.getPrimaryContainerToken()) != null + && mController.getContainer(splitData.getSecondaryContainerToken()) + != null) { + taskContainer.addSplitContainer( + new SplitContainer(splitData, mController, rule)); + } + } + + mController.onTaskFragmentParentRestored(wct, taskContainer.getTaskId(), + mTaskFragmentParentInfos.get(taskContainer.getTaskId())); + restoredAny = true; + } + + if (mParcelableTaskContainerDataList.isEmpty()) { + mTaskFragmentParentInfos.clear(); + mTaskFragmentInfos.clear(); + } + return restoredAny; + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java index 290fefa5abfa..882a8d035e93 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java @@ -169,6 +169,11 @@ class DividerPresenter implements View.OnTouchListener { @GuardedBy("mLock") private int mDividerPosition; + /** Indicates if there are containers to be finished since the divider has appeared. */ + @GuardedBy("mLock") + @VisibleForTesting + private boolean mHasContainersToFinish = false; + DividerPresenter(int taskId, @NonNull DragEventCallback dragEventCallback, @NonNull Executor callbackExecutor) { mTaskId = taskId; @@ -180,7 +185,8 @@ class DividerPresenter implements View.OnTouchListener { void updateDivider( @NonNull WindowContainerTransaction wct, @NonNull TaskFragmentParentInfo parentInfo, - @Nullable SplitContainer topSplitContainer) { + @Nullable SplitContainer topSplitContainer, + boolean isTaskFragmentVanished) { if (!Flags.activityEmbeddingInteractiveDividerFlag()) { return; } @@ -188,6 +194,18 @@ class DividerPresenter implements View.OnTouchListener { synchronized (mLock) { // Clean up the decor surface if top SplitContainer is null. if (topSplitContainer == null) { + // Check if there are containers to finish but the TaskFragment hasn't vanished yet. + // Don't remove the decor surface and divider if so as the removal should happen in + // a following step when the TaskFragment has vanished. This ensures that the decor + // surface is removed only after the resulting Activity is ready to be shown, + // otherwise there may be flicker. + if (mHasContainersToFinish) { + if (isTaskFragmentVanished) { + setHasContainersToFinish(false); + } else { + return; + } + } removeDecorSurfaceAndDivider(wct); return; } @@ -547,6 +565,7 @@ class DividerPresenter implements View.OnTouchListener { return true; } + // Only called by onTouch() and mRenderer is already null-checked. @GuardedBy("mLock") private void onStartDragging(@NonNull MotionEvent event) { mVelocityTracker = VelocityTracker.obtain(); @@ -572,6 +591,7 @@ class DividerPresenter implements View.OnTouchListener { }); } + // Only called by onTouch() and mRenderer is already null-checked. @GuardedBy("mLock") private void onDrag(@NonNull MotionEvent event) { if (mVelocityTracker != null) { @@ -642,8 +662,10 @@ class DividerPresenter implements View.OnTouchListener { @GuardedBy("mLock") private void updateDividerPosition(int position) { - mRenderer.setDividerPosition(position); - mRenderer.updateSurface(); + if (mRenderer != null) { + mRenderer.setDividerPosition(position); + mRenderer.updateSurface(); + } } @GuardedBy("mLock") @@ -651,7 +673,10 @@ class DividerPresenter implements View.OnTouchListener { // Veil visibility change should be applied together with the surface boost transaction in // the wct. final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - mRenderer.hideVeils(t); + + if (mRenderer != null) { + mRenderer.hideVeils(t); + } // Callbacks must be executed on the executor to release mLock and prevent deadlocks. // mDecorSurfaceOwner may change between here and when the callback is executed, @@ -666,8 +691,10 @@ class DividerPresenter implements View.OnTouchListener { } }); }); - mRenderer.mIsDragging = false; - mRenderer.mDragHandle.setPressed(mRenderer.mIsDragging); + if (mRenderer != null) { + mRenderer.mIsDragging = false; + mRenderer.mDragHandle.setPressed(mRenderer.mIsDragging); + } } /** @@ -868,11 +895,15 @@ class DividerPresenter implements View.OnTouchListener { } } + void setHasContainersToFinish(boolean hasContainersToFinish) { + synchronized (mLock) { + mHasContainersToFinish = hasContainersToFinish; + } + } + private static boolean isDraggingToFullscreenAllowed( @NonNull DividerAttributes dividerAttributes) { - // TODO(b/293654166) Use DividerAttributes.isDraggingToFullscreenAllowed when extension is - // updated to v7. - return false; + return dividerAttributes.isDraggingToFullscreenAllowed(); } /** @@ -1068,13 +1099,14 @@ class DividerPresenter implements View.OnTouchListener { @NonNull private final SurfaceControl mDividerSurface; @NonNull + private final SurfaceControl mDividerLineSurface; + @NonNull private final WindowlessWindowManager mWindowlessWindowManager; @NonNull private final SurfaceControlViewHost mViewHost; @NonNull private final FrameLayout mDividerLayout; - @NonNull - private final View mDividerLine; + @Nullable private View mDragHandle; @NonNull private final View.OnTouchListener mListener; @@ -1093,7 +1125,10 @@ class DividerPresenter implements View.OnTouchListener { mProperties = properties; mListener = listener; - mDividerSurface = createChildSurface("DividerSurface", true /* visible */); + mDividerSurface = createChildSurface( + mProperties.mDecorSurface, "DividerSurface", true /* visible */); + mDividerLineSurface = createChildSurface( + mDividerSurface, "DividerLineSurface", true /* visible */); mWindowlessWindowManager = new WindowlessWindowManager( mProperties.mConfiguration, mDividerSurface, @@ -1105,7 +1140,6 @@ class DividerPresenter implements View.OnTouchListener { context, displayManager.getDisplay(mProperties.mDisplayId), mWindowlessWindowManager, "DividerContainer"); mDividerLayout = new FrameLayout(context); - mDividerLine = new View(context); update(); } @@ -1198,6 +1232,7 @@ class DividerPresenter implements View.OnTouchListener { dividerSurfacePosition = mDividerPosition; } + // Update the divider surface position relative to the decor surface if (mProperties.mIsVerticalSplit) { t.setPosition(mDividerSurface, dividerSurfacePosition, 0.0f); t.setWindowCrop(mDividerSurface, mDividerSurfaceWidthPx, taskBounds.height()); @@ -1206,10 +1241,24 @@ class DividerPresenter implements View.OnTouchListener { t.setWindowCrop(mDividerSurface, taskBounds.width(), mDividerSurfaceWidthPx); } - // Update divider line position in the surface + // Update divider line surface position relative to the divider surface final int offset = mDividerPosition - dividerSurfacePosition; - mDividerLine.setX(mProperties.mIsVerticalSplit ? offset : 0); - mDividerLine.setY(mProperties.mIsVerticalSplit ? 0 : offset); + if (mProperties.mIsVerticalSplit) { + t.setPosition(mDividerLineSurface, offset, 0); + t.setWindowCrop(mDividerLineSurface, + mProperties.mDividerWidthPx, taskBounds.height()); + } else { + t.setPosition(mDividerLineSurface, 0, offset); + t.setWindowCrop(mDividerLineSurface, + taskBounds.width(), mProperties.mDividerWidthPx); + } + + // Update divider line surface visibility and color. + // If a container is fully expanded, the divider line is invisible unless dragging. + final boolean isDividerLineVisible = !mProperties.mIsDraggableExpandType || mIsDragging; + t.setVisibility(mDividerLineSurface, isDividerLineVisible); + t.setColor(mDividerLineSurface, colorToFloatArray( + Color.valueOf(mProperties.mDividerAttributes.getDividerColor()))); if (mIsDragging) { updateVeils(t); @@ -1255,21 +1304,6 @@ class DividerPresenter implements View.OnTouchListener { */ private void updateDivider(@NonNull SurfaceControl.Transaction t) { mDividerLayout.removeAllViews(); - mDividerLayout.addView(mDividerLine); - if (mProperties.mIsDraggableExpandType && !mIsDragging) { - // If a container is fully expanded, the divider overlays on the expanded container. - mDividerLine.setBackgroundColor(Color.TRANSPARENT); - } else { - mDividerLine.setBackgroundColor(mProperties.mDividerAttributes.getDividerColor()); - } - final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); - mDividerLine.setLayoutParams( - mProperties.mIsVerticalSplit - ? new FrameLayout.LayoutParams( - mProperties.mDividerWidthPx, taskBounds.height()) - : new FrameLayout.LayoutParams( - taskBounds.width(), mProperties.mDividerWidthPx) - ); if (mProperties.mDividerAttributes.getDividerType() == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { createVeils(); @@ -1323,10 +1357,11 @@ class DividerPresenter implements View.OnTouchListener { } @NonNull - private SurfaceControl createChildSurface(@NonNull String name, boolean visible) { + private SurfaceControl createChildSurface( + @NonNull SurfaceControl parent, @NonNull String name, boolean visible) { final Rect bounds = mProperties.mConfiguration.windowConfiguration.getBounds(); return new SurfaceControl.Builder() - .setParent(mProperties.mDecorSurface) + .setParent(parent) .setName(name) .setHidden(!visible) .setCallsite("DividerManager.createChildSurface") @@ -1337,10 +1372,12 @@ class DividerPresenter implements View.OnTouchListener { private void createVeils() { if (mPrimaryVeil == null) { - mPrimaryVeil = createChildSurface("DividerPrimaryVeil", false /* visible */); + mPrimaryVeil = createChildSurface( + mProperties.mDecorSurface, "DividerPrimaryVeil", false /* visible */); } if (mSecondaryVeil == null) { - mSecondaryVeil = createChildSurface("DividerSecondaryVeil", false /* visible */); + mSecondaryVeil = createChildSurface( + mProperties.mDecorSurface, "DividerSecondaryVeil", false /* visible */); } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index f9a6caf42e6e..9ea2943bc6da 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -17,6 +17,7 @@ package androidx.window.extensions.embedding; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.window.TaskFragmentAnimationParams.DEFAULT_ANIMATION_BACKGROUND_COLOR; import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_FRONT; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_DIM_ON_TASK; @@ -29,6 +30,7 @@ import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAs import static androidx.window.extensions.embedding.SplitContainer.shouldFinishPrimaryWithSecondary; import static androidx.window.extensions.embedding.SplitContainer.shouldFinishSecondaryWithPrimary; +import android.annotation.ColorInt; import android.app.Activity; import android.app.WindowConfiguration.WindowingMode; import android.content.Intent; @@ -48,6 +50,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; import java.util.Map; import java.util.concurrent.Executor; @@ -391,13 +394,34 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { if (splitAttributes == null) { return TaskFragmentAnimationParams.DEFAULT; } - final AnimationBackground animationBackground = splitAttributes.getAnimationBackground(); + final TaskFragmentAnimationParams.Builder builder = + new TaskFragmentAnimationParams.Builder(); + final int animationBackgroundColor = getAnimationBackgroundColor(splitAttributes); + builder.setAnimationBackgroundColor(animationBackgroundColor); + if (Flags.activityEmbeddingAnimationCustomizationFlag()) { + final int openAnimationResId = + splitAttributes.getAnimationParams().getOpenAnimationResId(); + builder.setOpenAnimationResId(openAnimationResId); + final int closeAnimationResId = + splitAttributes.getAnimationParams().getCloseAnimationResId(); + builder.setCloseAnimationResId(closeAnimationResId); + final int changeAnimationResId = + splitAttributes.getAnimationParams().getChangeAnimationResId(); + builder.setChangeAnimationResId(changeAnimationResId); + } + return builder.build(); + } + + @ColorInt + private static int getAnimationBackgroundColor(@NonNull SplitAttributes splitAttributes) { + int animationBackgroundColor = DEFAULT_ANIMATION_BACKGROUND_COLOR; + AnimationBackground animationBackground = splitAttributes.getAnimationBackground(); + if (Flags.activityEmbeddingAnimationCustomizationFlag()) { + animationBackground = splitAttributes.getAnimationParams().getAnimationBackground(); + } if (animationBackground instanceof AnimationBackground.ColorBackground colorBackground) { - return new TaskFragmentAnimationParams.Builder() - .setAnimationBackgroundColor(colorBackground.getColor()) - .build(); - } else { - return TaskFragmentAnimationParams.DEFAULT; + animationBackgroundColor = colorBackground.getColor(); } + return animationBackgroundColor; } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java new file mode 100644 index 000000000000..cb280c530c1b --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.embedding; + +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * This class holds the Parcelable data of a {@link SplitContainer}. + */ +class ParcelableSplitContainerData implements Parcelable { + + /** + * A reference to the target {@link SplitContainer} that owns the data. This will not be + * parcelled and will be {@code null} when the data is created from a parcel. + */ + @Nullable + final SplitContainer mSplitContainer; + + @NonNull + final IBinder mToken; + + @NonNull + private final IBinder mPrimaryContainerToken; + + @NonNull + private final IBinder mSecondaryContainerToken; + + // TODO(b/289875940): making this as non-null once the tag can be auto-generated from the rule. + @Nullable + final String mSplitRuleTag; + + /** + * Whether the selection of which container is primary can be changed at runtime. Runtime + * updates is currently possible only for {@link SplitPinContainer} + * + * @see SplitPinContainer + */ + final boolean mIsPrimaryContainerMutable; + + ParcelableSplitContainerData(@NonNull SplitContainer splitContainer, @NonNull IBinder token, + @NonNull IBinder primaryContainerToken, @NonNull IBinder secondaryContainerToken, + @Nullable String splitRuleTag, boolean isPrimaryContainerMutable) { + mSplitContainer = splitContainer; + mToken = token; + mPrimaryContainerToken = primaryContainerToken; + mSecondaryContainerToken = secondaryContainerToken; + mSplitRuleTag = splitRuleTag; + mIsPrimaryContainerMutable = isPrimaryContainerMutable; + } + + private ParcelableSplitContainerData(Parcel in) { + mSplitContainer = null; + mToken = in.readStrongBinder(); + mPrimaryContainerToken = in.readStrongBinder(); + mSecondaryContainerToken = in.readStrongBinder(); + mSplitRuleTag = in.readString(); + mIsPrimaryContainerMutable = in.readBoolean(); + } + + public static final Creator<ParcelableSplitContainerData> CREATOR = new Creator<>() { + @Override + public ParcelableSplitContainerData createFromParcel(Parcel in) { + return new ParcelableSplitContainerData(in); + } + + @Override + public ParcelableSplitContainerData[] newArray(int size) { + return new ParcelableSplitContainerData[size]; + } + }; + + @NonNull + IBinder getPrimaryContainerToken() { + return mSplitContainer != null ? mSplitContainer.getPrimaryContainer().getToken() + : mPrimaryContainerToken; + } + + @NonNull + IBinder getSecondaryContainerToken() { + return mSplitContainer != null ? mSplitContainer.getSecondaryContainer().getToken() + : mSecondaryContainerToken; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(mToken); + dest.writeStrongBinder(getPrimaryContainerToken()); + dest.writeStrongBinder(getSecondaryContainerToken()); + dest.writeString(mSplitRuleTag); + dest.writeBoolean(mIsPrimaryContainerMutable); + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java new file mode 100644 index 000000000000..97aa69985907 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.embedding; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; + +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class holds the Parcelable data of a {@link TaskContainer}. + */ +class ParcelableTaskContainerData implements Parcelable { + + /** + * A reference to the target {@link TaskContainer} that owns the data. This will not be + * parcelled and will be {@code null} when the data is created from a parcel. + */ + @Nullable + final TaskContainer mTaskContainer; + + /** + * The unique task id. + */ + final int mTaskId; + + /** + * The parcelable data of the active TaskFragmentContainers in this Task. + * Note that this will only be populated before parcelling, and will not be copied when + * making a new instance copy. + */ + @NonNull + private final List<ParcelableTaskFragmentContainerData> + mParcelableTaskFragmentContainerDataList = new ArrayList<>(); + + /** + * The parcelable data of the SplitContainers in this Task. + * Note that this will only be populated before parcelling, and will not be copied when + * making a new instance copy. + */ + @NonNull + private final List<ParcelableSplitContainerData> mParcelableSplitContainerDataList = + new ArrayList<>(); + + ParcelableTaskContainerData(int taskId, @NonNull TaskContainer taskContainer) { + if (taskId == INVALID_TASK_ID) { + throw new IllegalArgumentException("Invalid Task id"); + } + + mTaskId = taskId; + mTaskContainer = taskContainer; + } + + ParcelableTaskContainerData(@NonNull ParcelableTaskContainerData data, + @NonNull TaskContainer taskContainer) { + mTaskId = data.mTaskId; + mTaskContainer = taskContainer; + } + + private ParcelableTaskContainerData(Parcel in) { + mTaskId = in.readInt(); + mTaskContainer = null; + in.readParcelableList(mParcelableTaskFragmentContainerDataList, + ParcelableTaskFragmentContainerData.class.getClassLoader(), + ParcelableTaskFragmentContainerData.class); + in.readParcelableList(mParcelableSplitContainerDataList, + ParcelableSplitContainerData.class.getClassLoader(), + ParcelableSplitContainerData.class); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mTaskId); + dest.writeParcelableList(getParcelableTaskFragmentContainerDataList(), flags); + dest.writeParcelableList(getParcelableSplitContainerDataList(), flags); + } + + @NonNull + List<? extends ParcelableTaskFragmentContainerData> + getParcelableTaskFragmentContainerDataList() { + return mTaskContainer != null ? mTaskContainer.getParcelableTaskFragmentContainerDataList() + : mParcelableTaskFragmentContainerDataList; + } + + @NonNull + List<? extends ParcelableSplitContainerData> getParcelableSplitContainerDataList() { + return mTaskContainer != null ? mTaskContainer.getParcelableSplitContainerDataList() + : mParcelableSplitContainerDataList; + } + + @NonNull + List<String> getSplitRuleTags() { + final List<String> tags = new ArrayList<>(); + for (ParcelableSplitContainerData data : getParcelableSplitContainerDataList()) { + tags.add(data.mSplitRuleTag); + } + return tags; + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<ParcelableTaskContainerData> CREATOR = new Creator<>() { + @Override + public ParcelableTaskContainerData createFromParcel(Parcel in) { + return new ParcelableTaskContainerData(in); + } + + @Override + public ParcelableTaskContainerData[] newArray(int size) { + return new ParcelableTaskContainerData[size]; + } + }; +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskFragmentContainerData.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskFragmentContainerData.java new file mode 100644 index 000000000000..a79a89a210ac --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskFragmentContainerData.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.embedding; + +import android.app.Activity; +import android.graphics.Rect; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * This class holds the Parcelable data of a {@link TaskFragmentContainer}. + */ +class ParcelableTaskFragmentContainerData implements Parcelable { + + /** + * Client-created token that uniquely identifies the task fragment container instance. + */ + @NonNull + final IBinder mToken; + + /** + * The tag specified in launch options. {@code null} if this taskFragment container is not an + * overlay container. + */ + @Nullable + final String mOverlayTag; + + /** + * The associated {@link Activity#getActivityToken()} of the overlay container. + * Must be {@code null} for non-overlay container. + * <p> + * If an overlay container is associated with an activity, this overlay container will be + * dismissed when the associated activity is destroyed. If the overlay container is visible, + * activity will be launched on top of the overlay container and expanded to fill the parent + * container. + */ + @Nullable + final IBinder mAssociatedActivityToken; + + /** + * Bounds that were requested last via {@link android.window.WindowContainerTransaction}. + */ + @NonNull + final Rect mLastRequestedBounds; + + ParcelableTaskFragmentContainerData(@NonNull IBinder token, @Nullable String overlayTag, + @Nullable IBinder associatedActivityToken) { + mToken = token; + mOverlayTag = overlayTag; + mAssociatedActivityToken = associatedActivityToken; + mLastRequestedBounds = new Rect(); + } + + private ParcelableTaskFragmentContainerData(Parcel in) { + mToken = in.readStrongBinder(); + mOverlayTag = in.readString(); + mAssociatedActivityToken = in.readStrongBinder(); + mLastRequestedBounds = in.readTypedObject(Rect.CREATOR); + } + + public static final Creator<ParcelableTaskFragmentContainerData> CREATOR = new Creator<>() { + @Override + public ParcelableTaskFragmentContainerData createFromParcel(Parcel in) { + return new ParcelableTaskFragmentContainerData(in); + } + + @Override + public ParcelableTaskFragmentContainerData[] newArray(int size) { + return new ParcelableTaskFragmentContainerData[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(mToken); + dest.writeString(mOverlayTag); + dest.writeStrongBinder(mAssociatedActivityToken); + dest.writeTypedObject(mLastRequestedBounds, flags); + } + +} + diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java index 39cfacec8447..faf73c24073f 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java @@ -33,6 +33,8 @@ import androidx.window.extensions.core.util.function.Function; */ class SplitContainer { @NonNull + private final ParcelableSplitContainerData mParcelableData; + @NonNull private TaskFragmentContainer mPrimaryContainer; @NonNull private final TaskFragmentContainer mSecondaryContainer; @@ -44,16 +46,6 @@ class SplitContainer { /** @see SplitContainer#getDefaultSplitAttributes() */ @NonNull private SplitAttributes mDefaultSplitAttributes; - @NonNull - private final IBinder mToken; - - /** - * Whether the selection of which container is primary can be changed at runtime. Runtime - * updates is currently possible only for {@link SplitPinContainer} - * - * @see SplitPinContainer - */ - private final boolean mIsPrimaryContainerMutable; SplitContainer(@NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity, @@ -69,13 +61,14 @@ class SplitContainer { @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule, @NonNull SplitAttributes splitAttributes, boolean isPrimaryContainerMutable) { + mParcelableData = new ParcelableSplitContainerData(this, new Binder("SplitContainer"), + primaryContainer.getToken(), secondaryContainer.getToken(), splitRule.getTag(), + isPrimaryContainerMutable); mPrimaryContainer = primaryContainer; mSecondaryContainer = secondaryContainer; mSplitRule = splitRule; mDefaultSplitAttributes = splitRule.getDefaultSplitAttributes(); mCurrentSplitAttributes = splitAttributes; - mToken = new Binder("SplitContainer"); - mIsPrimaryContainerMutable = isPrimaryContainerMutable; if (shouldFinishPrimaryWithSecondary(splitRule)) { if (mPrimaryContainer.getRunningActivityCount() == 1 @@ -93,8 +86,27 @@ class SplitContainer { } } + /** This is only used when restoring it from a {@link ParcelableSplitContainerData}. */ + SplitContainer(@NonNull ParcelableSplitContainerData parcelableData, + @NonNull SplitController splitController, @NonNull SplitRule splitRule) { + mParcelableData = parcelableData; + mPrimaryContainer = splitController.getContainer(parcelableData.getPrimaryContainerToken()); + mSecondaryContainer = splitController.getContainer( + parcelableData.getSecondaryContainerToken()); + mSplitRule = splitRule; + mDefaultSplitAttributes = splitRule.getDefaultSplitAttributes(); + mCurrentSplitAttributes = mDefaultSplitAttributes; + + if (shouldFinishPrimaryWithSecondary(splitRule)) { + mSecondaryContainer.addContainerToFinishOnExit(mPrimaryContainer); + } + if (shouldFinishSecondaryWithPrimary(splitRule)) { + mPrimaryContainer.addContainerToFinishOnExit(mSecondaryContainer); + } + } + void setPrimaryContainer(@NonNull TaskFragmentContainer primaryContainer) { - if (!mIsPrimaryContainerMutable) { + if (!mParcelableData.mIsPrimaryContainerMutable) { throw new IllegalStateException("Cannot update primary TaskFragmentContainer"); } mPrimaryContainer = primaryContainer; @@ -150,7 +162,12 @@ class SplitContainer { @NonNull IBinder getToken() { - return mToken; + return mParcelableData.mToken; + } + + @NonNull + ParcelableSplitContainerData getParcelableData() { + return mParcelableData; } /** @@ -201,7 +218,7 @@ class SplitContainer { return null; } return new SplitInfo(primaryActivityStack, secondaryActivityStack, - mCurrentSplitAttributes, SplitInfo.Token.createFromBinder(mToken)); + mCurrentSplitAttributes, SplitInfo.Token.createFromBinder(mParcelableData.mToken)); } static boolean shouldFinishPrimaryWithSecondary(@NonNull SplitRule splitRule) { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index b850dbc24f25..db4bb0e5e75e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.window.ActivityWindowInfo.getActivityWindowInfo; import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE; @@ -57,18 +58,22 @@ import android.app.Activity; import android.app.ActivityClient; import android.app.ActivityOptions; import android.app.ActivityThread; +import android.app.AppGlobals; import android.app.Application; import android.app.Instrumentation; import android.app.servertransaction.ClientTransactionListenerController; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import android.os.RemoteException; +import android.os.SystemProperties; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; @@ -80,6 +85,7 @@ import android.window.ActivityWindowInfo; import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentInfo; import android.window.TaskFragmentOperation; +import android.window.TaskFragmentOrganizer; import android.window.TaskFragmentParentInfo; import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; @@ -87,9 +93,9 @@ import android.window.WindowContainerTransaction; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.window.common.CommonFoldingFeature; import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.common.EmptyLifecycleCallbacksAdapter; +import androidx.window.common.layout.CommonFoldingFeature; import androidx.window.extensions.WindowExtensions; import androidx.window.extensions.core.util.function.Consumer; import androidx.window.extensions.core.util.function.Function; @@ -114,11 +120,12 @@ import java.util.function.BiConsumer; public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback, ActivityEmbeddingComponent, DividerPresenter.DragEventCallback { static final String TAG = "SplitController"; - static final boolean ENABLE_SHELL_TRANSITIONS = true; + static final boolean ENABLE_SHELL_TRANSITIONS = getShellTransitEnabled(); // TODO(b/243518738): Move to WM Extensions if we have requirement of overlay without // association. It's not set in WM Extensions nor Wm Jetpack library currently. - private static final String KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY = + @VisibleForTesting + static final String KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY = "androidx.window.extensions.embedding.shouldAssociateWithLaunchingActivity"; @VisibleForTesting @@ -205,11 +212,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** Listener registered to {@link ClientTransactionListenerController}. */ @GuardedBy("mLock") - @Nullable + @NonNull private final BiConsumer<IBinder, ActivityWindowInfo> mActivityWindowInfoListener = - Flags.activityWindowInfoFlag() - ? this::onActivityWindowInfoChanged - : null; + this::onActivityWindowInfoChanged; private final Handler mHandler; private final MainThreadExecutor mExecutor; @@ -274,6 +279,26 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen Log.i(TAG, "Setting embedding rules. Size: " + rules.size()); mSplitRules.clear(); mSplitRules.addAll(rules); + + if (!Flags.aeBackStackRestore() || !mPresenter.isRebuildTaskContainersNeeded()) { + return; + } + + try { + final TransactionRecord transactionRecord = + mTransactionManager.startNewTransaction(); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); + if (mPresenter.rebuildTaskContainers(wct, rules)) { + transactionRecord.apply(false /* shouldApplyIndependently */); + updateCallbackIfNecessary(); + } else { + transactionRecord.abort(); + } + } catch (IllegalStateException ex) { + Log.e(TAG, "Having an existing transaction while running restoration with" + + "new rules!! It is likely too late to perform the restoration " + + "already!?", ex); + } } } @@ -673,7 +698,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen break; case TYPE_TASK_FRAGMENT_VANISHED: mPresenter.removeTaskFragmentInfo(info); - onTaskFragmentVanished(wct, info); + onTaskFragmentVanished(wct, info, taskId); break; case TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED: onTaskFragmentParentInfoChanged(wct, taskId, @@ -695,12 +720,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen break; case TYPE_ACTIVITY_REPARENTED_TO_TASK: final IBinder candidateAssociatedActToken, lastOverlayToken; - if (Flags.fixPipRestoreToOverlay()) { - candidateAssociatedActToken = change.getOtherActivityToken(); - lastOverlayToken = change.getTaskFragmentToken(); - } else { - candidateAssociatedActToken = lastOverlayToken = null; - } + candidateAssociatedActToken = change.getOtherActivityToken(); + lastOverlayToken = change.getTaskFragmentToken(); onActivityReparentedToTask( wct, taskId, @@ -834,7 +855,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @VisibleForTesting @GuardedBy("mLock") void onTaskFragmentVanished(@NonNull WindowContainerTransaction wct, - @NonNull TaskFragmentInfo taskFragmentInfo) { + @NonNull TaskFragmentInfo taskFragmentInfo, int taskId) { final TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); if (container != null) { // Cleanup if the TaskFragment vanished is not requested by the organizer. @@ -843,6 +864,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen updateContainersInTaskIfVisible(wct, container.getTaskId()); } cleanupTaskFragment(taskFragmentInfo.getFragmentToken()); + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer != null) { + // Update the divider to clean up any decor surfaces. + updateDivider(wct, taskContainer, true /* isTaskFragmentVanished */); + } } /** @@ -884,7 +910,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // The divider need to be updated even if shouldUpdateContainer is false, because the decor // surface may change in TaskFragmentParentInfo, which requires divider update but not // container update. - updateDivider(wct, taskContainer); + updateDivider(wct, taskContainer, false /* isTaskFragmentVanished */); // If the last direct activity of the host task is dismissed and there's an always-on-top // overlay container in the task, the overlay container should also be dismissed. @@ -897,6 +923,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } @GuardedBy("mLock") + void onTaskFragmentParentRestored(@NonNull WindowContainerTransaction wct, int taskId, + @NonNull TaskFragmentParentInfo parentInfo) { + onTaskFragmentParentInfoChanged(wct, taskId, parentInfo); + } + + @GuardedBy("mLock") void updateContainersInTaskIfVisible(@NonNull WindowContainerTransaction wct, int taskId) { final TaskContainer taskContainer = getTaskContainer(taskId); if (taskContainer == null) { @@ -905,7 +937,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (taskContainer.isVisible()) { updateContainersInTask(wct, taskContainer); - } else if (Flags.fixNoContainerUpdateWithoutResize()) { + } else { // the TaskFragmentContainers need to be updated when the task becomes visible taskContainer.mTaskFragmentContainersNeedsUpdate = true; } @@ -1019,10 +1051,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Nullable OverlayContainerRestoreParams getOverlayContainerRestoreParams( @Nullable IBinder associatedActivityToken, @Nullable IBinder overlayToken) { - if (!Flags.fixPipRestoreToOverlay()) { - return null; - } - if (associatedActivityToken == null || overlayToken == null) { return null; } @@ -1330,13 +1358,24 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (shouldContainerBeExpanded(container)) { // Make sure that the existing container is expanded. mPresenter.expandTaskFragment(wct, container); - } else { - // Put activity into a new expanded container. - final TaskFragmentContainer newContainer = - new TaskFragmentContainer.Builder(this, getTaskId(activity), activity) - .setPendingAppearedActivity(activity).build(); - mPresenter.expandActivity(wct, newContainer.getTaskFragmentToken(), activity); + return; + } + + final SplitContainer splitContainer = getActiveSplitForContainer(container); + if (splitContainer instanceof SplitPinContainer + && !container.isPinned() && container.getRunningActivityCount() == 1) { + // This is already the expected state when the pinned container is shown with an + // expanded activity in a standalone container on the side. Moving the activity into + // another new expanded container again is not necessary and could result in + // recursively creating new TaskFragmentContainers if the activity somehow relaunched. + return; } + + // Put activity into a new expanded container. + final TaskFragmentContainer newContainer = + new TaskFragmentContainer.Builder(this, getTaskId(activity), activity) + .setPendingAppearedActivity(activity).build(); + mPresenter.expandActivity(wct, newContainer.getTaskFragmentToken(), activity); } /** @@ -2529,6 +2568,21 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return mTaskContainers.get(taskId); } + @NonNull + @GuardedBy("mLock") + List<TaskContainer> getTaskContainers() { + final ArrayList<TaskContainer> taskContainers = new ArrayList<>(); + for (int i = mTaskContainers.size() - 1; i >= 0; i--) { + taskContainers.add(mTaskContainers.valueAt(i)); + } + return taskContainers; + } + + @GuardedBy("mLock") + void setSavedState(@NonNull Bundle savedState) { + mPresenter.setSavedState(savedState); + } + @GuardedBy("mLock") void addTaskContainer(int taskId, TaskContainer taskContainer) { mTaskContainers.put(taskId, taskContainer); @@ -2552,9 +2606,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return ActivityThread.currentActivityThread().getActivity(activityToken); } - @VisibleForTesting @Nullable - ActivityThread.ActivityClientRecord getActivityClientRecord(@NonNull Activity activity) { + private ActivityThread.ActivityClientRecord getActivityClientRecord( + @NonNull Activity activity) { return ActivityThread.currentActivityThread() .getActivityClient(activity.getActivityToken()); } @@ -2710,15 +2764,19 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen TaskFragmentContainer createOrUpdateOverlayTaskFragmentIfNeeded( @NonNull WindowContainerTransaction wct, @NonNull Bundle options, @NonNull Intent intent, @NonNull Activity launchActivity) { + final String overlayTag = Objects.requireNonNull(options.getString(KEY_OVERLAY_TAG)); if (isActivityFromSplit(launchActivity)) { // We restrict to launch the overlay from split. Fallback to treat it as normal // launch. + Log.w(TAG, "It's not allowed to launch overlay container with tag=" + overlayTag + + " from activity in Activity Embedding split." + + " Launching activity=" + launchActivity + + " Fallback to launch the activity as normal launch."); return null; } final List<TaskFragmentContainer> overlayContainers = getAllNonFinishingOverlayContainers(); - final String overlayTag = Objects.requireNonNull(options.getString(KEY_OVERLAY_TAG)); final boolean associateLaunchingActivity = options .getBoolean(KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY, true); @@ -2739,89 +2797,70 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } final int taskId = getTaskId(launchActivity); - if (!overlayContainers.isEmpty()) { - for (final TaskFragmentContainer overlayContainer : overlayContainers) { - final boolean isTopNonFinishingOverlay = overlayContainer.equals( - overlayContainer.getTaskContainer().getTopNonFinishingTaskFragmentContainer( - true /* includePin */, true /* includeOverlay */)); - if (taskId != overlayContainer.getTaskId()) { - // If there's an overlay container with same tag in a different task, - // dismiss the overlay container since the tag must be unique per process. - if (overlayTag.equals(overlayContainer.getOverlayTag())) { - Log.w(TAG, "The overlay container with tag:" - + overlayContainer.getOverlayTag() + " is dismissed because" - + " there's an existing overlay container with the same tag but" - + " different task ID:" + overlayContainer.getTaskId() + ". " - + "The new associated activity is " + launchActivity); - mPresenter.cleanupContainer(wct, overlayContainer, - false /* shouldFinishDependant */); - } - continue; - } - if (!overlayTag.equals(overlayContainer.getOverlayTag())) { - // If there's an overlay container with different tag on top in the same - // task, dismiss the existing overlay container. - if (isTopNonFinishingOverlay) { - mPresenter.cleanupContainer(wct, overlayContainer, - false /* shouldFinishDependant */); - } - continue; - } - // The overlay container has the same tag and task ID with the new launching - // overlay container. - if (!isTopNonFinishingOverlay) { - // Dismiss the invisible overlay container regardless of activity - // association if it collides the tag of new launched overlay container . - Log.w(TAG, "The invisible overlay container with tag:" - + overlayContainer.getOverlayTag() + " is dismissed because" - + " there's a launching overlay container with the same tag." - + " The new associated activity is " + launchActivity); - mPresenter.cleanupContainer(wct, overlayContainer, - false /* shouldFinishDependant */); - continue; - } - // Requesting an always-on-top overlay. - if (!associateLaunchingActivity) { - if (overlayContainer.isOverlayWithActivityAssociation()) { - // Dismiss the overlay container since it has associated with an activity. - Log.w(TAG, "The overlay container with tag:" - + overlayContainer.getOverlayTag() + " is dismissed because" - + " there's an existing overlay container with the same tag but" - + " different associated launching activity. The overlay container" - + " doesn't associate with any activity."); - mPresenter.cleanupContainer(wct, overlayContainer, - false /* shouldFinishDependant */); - continue; - } else { - // The existing overlay container doesn't associate an activity as well. - // Just update the overlay and return. - // Note that going to this condition means the tag, task ID matches a - // visible always-on-top overlay, and won't dismiss any overlay any more. - mPresenter.applyActivityStackAttributes(wct, overlayContainer, attrs, - getMinDimensions(intent)); - return overlayContainer; - } - } - if (launchActivity.getActivityToken() - != overlayContainer.getAssociatedActivityToken()) { - Log.w(TAG, "The overlay container with tag:" - + overlayContainer.getOverlayTag() + " is dismissed because" - + " there's an existing overlay container with the same tag but" - + " different associated launching activity. The new associated" - + " activity is " + launchActivity); - // The associated activity must be the same, or it will be dismissed. - mPresenter.cleanupContainer(wct, overlayContainer, - false /* shouldFinishDependant */); - continue; - } - // Reaching here means the launching activity launch an overlay container with the - // same task ID, tag, while there's a previously launching visible overlay - // container. We'll regard it as updating the existing overlay container. + // Overlay container policy: + // 1. Overlay tag must be unique per process. + // a. For associated overlay, if a new launched overlay container has the same tag as + // an existing one, the existing overlay will be dismissed regardless of its task + // and window hierarchy. + // b. For always-on-top overlay, if there's an overlay container has the same tag in the + // launched task, the overlay container will be re-used, which means the + // ActivityStackAttributes will be applied and the launched activity will be positioned + // on top of the overlay container. + // 2. There must be at most one overlay that partially occludes a visible activity per task. + // a. For associated overlay, only the top visible overlay container in the launched task + // will be dismissed. + // b. Always-on-top overlay is always visible. If there's an overlay with different tags + // in the same task, the overlay will be dismissed in case an activity above + // the overlay is dismissed and the overlay is shown unexpectedly. + for (final TaskFragmentContainer overlayContainer : overlayContainers) { + final boolean isTopNonFinishingOverlay = overlayContainer.isTopNonFinishingChild(); + final boolean areInSameTask = taskId == overlayContainer.getTaskId(); + final boolean haveSameTag = overlayTag.equals(overlayContainer.getOverlayTag()); + if (!associateLaunchingActivity && overlayContainer.isAlwaysOnTopOverlay() + && haveSameTag && areInSameTask) { + // Just launch the activity and update the existing always-on-top overlay + // if the requested overlay is an always-on-top overlay with the same tag + // as the existing one. mPresenter.applyActivityStackAttributes(wct, overlayContainer, attrs, getMinDimensions(intent)); return overlayContainer; - } + if (haveSameTag) { + // For other tag match, we should clean up the existing overlay since the overlay + // tag must be unique per process. + Log.w(TAG, "The overlay container with tag:" + + overlayContainer.getOverlayTag() + " is dismissed with " + + " the launching activity=" + launchActivity + + " because there's an existing overlay container with the same tag."); + mPresenter.cleanupContainer(wct, overlayContainer, + false /* shouldFinishDependant */); + } + if (!areInSameTask) { + // Early return here because we won't clean-up or update overlay from different + // tasks except tag collision. + continue; + } + if (associateLaunchingActivity) { + // For associated overlay, we only dismiss the overlay if it's the top non-finishing + // child of its parent container. + if (isTopNonFinishingOverlay) { + Log.w(TAG, "The on-top overlay container with tag:" + + overlayContainer.getOverlayTag() + " is dismissed with " + + " the launching activity=" + launchActivity + + "because we only allow one overlay on top."); + mPresenter.cleanupContainer(wct, overlayContainer, + false /* shouldFinishDependant */); + } + continue; + } + // Otherwise, we should clean up the overlay in the task because we only allow one + // overlay when an always-on-top overlay is launched. + Log.w(TAG, "The overlay container with tag:" + + overlayContainer.getOverlayTag() + " is dismissed with " + + " the launching activity=" + launchActivity + + "because an always-on-top overlay is launched."); + mPresenter.cleanupContainer(wct, overlayContainer, + false /* shouldFinishDependant */); } // Launch the overlay container to the task with taskId. return createEmptyContainer(wct, intent, taskId, attrs, launchActivity, overlayTag, @@ -2837,6 +2876,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return getActiveSplitForContainer(container) != null; } + void scheduleBackup() { + synchronized (mLock) { + mPresenter.scheduleBackup(); + } + } + private final class LifecycleCallbacks extends EmptyLifecycleCallbacksAdapter { @Override @@ -3091,22 +3136,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ @Override public boolean isActivityEmbedded(@NonNull Activity activity) { - Objects.requireNonNull(activity); synchronized (mLock) { - if (Flags.activityWindowInfoFlag()) { - final ActivityWindowInfo activityWindowInfo = getActivityWindowInfo(activity); - return activityWindowInfo != null && activityWindowInfo.isEmbedded(); - } - return mPresenter.isActivityEmbedded(activity.getActivityToken()); + return TaskFragmentOrganizer.isActivityEmbedded(activity); } } @Override public void setEmbeddedActivityWindowInfoCallback(@NonNull Executor executor, @NonNull Consumer<EmbeddedActivityWindowInfo> callback) { - if (!Flags.activityWindowInfoFlag()) { - return; - } Objects.requireNonNull(executor); Objects.requireNonNull(callback); synchronized (mLock) { @@ -3120,9 +3157,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Override public void clearEmbeddedActivityWindowInfoCallback() { - if (!Flags.activityWindowInfoFlag()) { - return; - } synchronized (mLock) { if (mEmbeddedActivityWindowInfoCallback == null) { return; @@ -3143,9 +3177,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Nullable @Override public EmbeddedActivityWindowInfo getEmbeddedActivityWindowInfo(@NonNull Activity activity) { - if (!Flags.activityWindowInfoFlag()) { - return null; - } synchronized (mLock) { final ActivityWindowInfo activityWindowInfo = getActivityWindowInfo(activity); return activityWindowInfo != null @@ -3176,15 +3207,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } } - @Nullable - private ActivityWindowInfo getActivityWindowInfo(@NonNull Activity activity) { - if (activity.isFinishing()) { - return null; - } - final ActivityThread.ActivityClientRecord record = getActivityClientRecord(activity); - return record != null ? record.getActivityWindowInfo() : null; - } - @NonNull private static EmbeddedActivityWindowInfo translateActivityWindowInfo( @NonNull Activity activity, @NonNull ActivityWindowInfo activityWindowInfo) { @@ -3267,12 +3289,15 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } @GuardedBy("mLock") - void updateDivider( - @NonNull WindowContainerTransaction wct, @NonNull TaskContainer taskContainer) { + void updateDivider(@NonNull WindowContainerTransaction wct, + @NonNull TaskContainer taskContainer, boolean isTaskFragmentVanished) { final DividerPresenter dividerPresenter = mDividerPresenters.get(taskContainer.getTaskId()); final TaskFragmentParentInfo parentInfo = taskContainer.getTaskFragmentParentInfo(); - dividerPresenter.updateDivider( - wct, parentInfo, taskContainer.getTopNonFinishingSplitContainer()); + final SplitContainer topSplitContainer = taskContainer.getTopNonFinishingSplitContainer(); + if (dividerPresenter != null) { + dividerPresenter.updateDivider( + wct, parentInfo, topSplitContainer, isTaskFragmentVanished); + } } @Override @@ -3302,6 +3327,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final List<TaskFragmentContainer> containersToFinish = new ArrayList<>(); taskContainer.updateTopSplitContainerForDivider( dividerPresenter, containersToFinish); + if (!containersToFinish.isEmpty()) { + dividerPresenter.setHasContainersToFinish(true); + } for (final TaskFragmentContainer container : containersToFinish) { mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); } @@ -3311,4 +3339,17 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen transactionRecord.apply(false /* shouldApplyIndependently */); } } + + // TODO(b/207070762): cleanup with legacy app transition + private static boolean getShellTransitEnabled() { + try { + if (AppGlobals.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_AUTOMOTIVE, 0)) { + return SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); + } + } catch (RemoteException re) { + Log.w(TAG, "Error getting system features"); + } + return true; + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index d888fa9d6feb..0c0ded9bad74 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -20,8 +20,11 @@ import static android.content.pm.PackageManager.MATCH_ALL; import static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider; import static androidx.window.extensions.embedding.SplitAttributesHelper.isReversedLayout; +import static androidx.window.extensions.embedding.SplitController.TAG; import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK; +import android.annotation.AnimRes; +import android.annotation.NonNull; import android.app.Activity; import android.app.ActivityThread; import android.app.WindowConfiguration; @@ -31,9 +34,11 @@ import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; +import android.util.Log; import android.util.Pair; import android.util.Size; import android.view.View; @@ -43,7 +48,6 @@ import android.window.TaskFragmentCreationParams; import android.window.WindowContainerTransaction; import androidx.annotation.IntDef; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.window.extensions.core.util.function.Function; import androidx.window.extensions.embedding.SplitAttributes.SplitType; @@ -56,12 +60,14 @@ import androidx.window.extensions.layout.FoldingFeature; import androidx.window.extensions.layout.WindowLayoutComponentImpl; import androidx.window.extensions.layout.WindowLayoutInfo; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.window.flags.Flags; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.concurrent.Executor; /** @@ -125,6 +131,16 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { static final int RESULT_EXPAND_FAILED_NO_TF_INFO = 2; /** + * The key of {@link ActivityStack} alignment relative to its parent container. + * <p> + * See {@link ContainerPosition} for possible values. + * <p> + * Note that this constants must align with the definition in WM Jetpack library. + */ + private static final String KEY_ACTIVITY_STACK_ALIGNMENT = + "androidx.window.embedding.ActivityStackAlignment"; + + /** * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, * Activity, Activity, Intent)} */ @@ -143,6 +159,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { private final WindowLayoutComponentImpl mWindowLayoutComponent; private final SplitController mController; + @NonNull + private final BackupHelper mBackupHelper; SplitPresenter(@NonNull Executor executor, @NonNull WindowLayoutComponentImpl windowLayoutComponent, @@ -150,7 +168,14 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { super(executor, controller); mWindowLayoutComponent = windowLayoutComponent; mController = controller; - registerOrganizer(); + final Bundle outSavedState = new Bundle(); + if (Flags.aeBackStackRestore()) { + outSavedState.setClassLoader(ParcelableTaskContainerData.class.getClassLoader()); + registerOrganizer(false /* isSystemOrganizer */, outSavedState); + } else { + registerOrganizer(); + } + mBackupHelper = new BackupHelper(controller, this, outSavedState); if (!SplitController.ENABLE_SHELL_TRANSITIONS) { // TODO(b/207070762): cleanup with legacy app transition // Animation will be handled by WM Shell when Shell transition is enabled. @@ -158,6 +183,19 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } } + void scheduleBackup() { + mBackupHelper.scheduleBackup(); + } + + boolean isRebuildTaskContainersNeeded() { + return mBackupHelper.hasPendingStateToRestore(); + } + + boolean rebuildTaskContainers(@NonNull WindowContainerTransaction wct, + @NonNull Set<EmbeddingRule> rules) { + return mBackupHelper.rebuildTaskContainers(wct, rules); + } + /** * Deletes the specified container and all other associated and dependent containers in the same * transaction. @@ -374,7 +412,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode); updateAnimationParams(wct, primaryContainer.getTaskFragmentToken(), splitAttributes); updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes); - mController.updateDivider(wct, taskContainer); + mController.updateDivider(wct, taskContainer, false /* isTaskFragmentVanished */); } private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, @@ -395,8 +433,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // Sets the dim area when the two TaskFragments are adjacent. final boolean dimOnTask = !isStacked - && splitAttributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK - && Flags.fullscreenDimFlag(); + && splitAttributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK; setTaskFragmentDimOnTask(wct, primaryContainer.getTaskFragmentToken(), dimOnTask); setTaskFragmentDimOnTask(wct, secondaryContainer.getTaskFragmentToken(), dimOnTask); @@ -636,7 +673,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { container); final boolean isFillParent = relativeBounds.isEmpty(); final boolean dimOnTask = !isFillParent - && Flags.fullscreenDimFlag() && attributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK; final IBinder fragmentToken = container.getTaskFragmentToken(); @@ -649,14 +685,114 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // TODO(b/243518738): Update to resizeTaskFragment after we migrate WCT#setRelativeBounds // and WCT#setWindowingMode to take fragmentToken. resizeTaskFragmentIfRegistered(wct, container, relativeBounds); - int windowingMode = container.getTaskContainer().getWindowingModeForTaskFragment( - relativeBounds); + final TaskContainer taskContainer = container.getTaskContainer(); + final int windowingMode = taskContainer.getWindowingModeForTaskFragment(relativeBounds); updateTaskFragmentWindowingModeIfRegistered(wct, container, windowingMode); - // Always use default animation for standalone ActivityStack. - updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT); + if (container.isOverlay() && isOverlayTransitionSupported()) { + // Use the overlay transition for the overlay container if it's supported. + final TaskFragmentAnimationParams params = createOverlayAnimationParams(relativeBounds, + taskContainer.getBounds(), container); + updateAnimationParams(wct, fragmentToken, params); + } else { + // Otherwise, fallabck to use the default animation params. + updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT); + } setTaskFragmentDimOnTask(wct, fragmentToken, dimOnTask); } + private static boolean isOverlayTransitionSupported() { + return Flags.moveAnimationOptionsToChange() + && Flags.activityEmbeddingOverlayPresentationFlag(); + } + + @NonNull + private static TaskFragmentAnimationParams createOverlayAnimationParams( + @NonNull Rect relativeBounds, @NonNull Rect parentContainerBounds, + @NonNull TaskFragmentContainer container) { + if (relativeBounds.isEmpty()) { + return TaskFragmentAnimationParams.DEFAULT; + } + + final int positionFromOptions = container.getLaunchOptions() + .getInt(KEY_ACTIVITY_STACK_ALIGNMENT , -1); + final int position = positionFromOptions != -1 ? positionFromOptions + // Fallback to calculate from bounds if the info can't be retrieved from options. + : getOverlayPosition(relativeBounds, parentContainerBounds); + + return new TaskFragmentAnimationParams.Builder() + .setOpenAnimationResId(getOpenAnimationResourcesId(position)) + .setChangeAnimationResId(R.anim.overlay_task_fragment_change) + .setCloseAnimationResId(getCloseAnimationResourcesId(position)) + .build(); + } + + @VisibleForTesting + @ContainerPosition + static int getOverlayPosition( + @NonNull Rect relativeBounds, @NonNull Rect parentContainerBounds) { + final Rect relativeParentBounds = new Rect(parentContainerBounds); + relativeParentBounds.offsetTo(0, 0); + final int leftMatch = (relativeParentBounds.left == relativeBounds.left) ? 1 : 0; + final int topMatch = (relativeParentBounds.top == relativeBounds.top) ? 1 : 0; + final int rightMatch = (relativeParentBounds.right == relativeBounds.right) ? 1 : 0; + final int bottomMatch = (relativeParentBounds.bottom == relativeBounds.bottom) ? 1 : 0; + + // Flag format: {left|top|right|bottom}. Note that overlay container could be shrunk and + // centered, which makes only one of overlay container edge matches the parent container. + final int directionFlag = (leftMatch << 3) + (topMatch << 2) + (rightMatch << 1) + + bottomMatch; + + final int position = switch (directionFlag) { + // Only the left edge match or only the right edge not match: should be on the left of + // the parent container. + case 0b1000, 0b1101 -> CONTAINER_POSITION_LEFT; + // Only the top edge match or only the bottom edge not match: should be on the top of + // the parent container. + case 0b0100, 0b1110 -> CONTAINER_POSITION_TOP; + // Only the right edge match or only the left edge not match: should be on the right of + // the parent container. + case 0b0010, 0b0111 -> CONTAINER_POSITION_RIGHT; + // Only the bottom edge match or only the top edge not match: should be on the bottom of + // the parent container. + case 0b0001, 0b1011 -> CONTAINER_POSITION_BOTTOM; + default -> { + Log.w(TAG, "Unsupported position:" + Integer.toBinaryString(directionFlag) + + " fallback to treat it as right. Relative parent bounds: " + + relativeParentBounds + ", relative overlay bounds:" + relativeBounds); + yield CONTAINER_POSITION_RIGHT; + } + }; + return position; + } + + @AnimRes + private static int getOpenAnimationResourcesId(@ContainerPosition int position) { + return switch (position) { + case CONTAINER_POSITION_LEFT -> R.anim.overlay_task_fragment_open_from_left; + case CONTAINER_POSITION_TOP -> R.anim.overlay_task_fragment_open_from_top; + case CONTAINER_POSITION_RIGHT -> R.anim.overlay_task_fragment_open_from_right; + case CONTAINER_POSITION_BOTTOM -> R.anim.overlay_task_fragment_open_from_bottom; + default -> { + Log.w(TAG, "Unknown position:" + position); + yield Resources.ID_NULL; + } + }; + } + + @AnimRes + private static int getCloseAnimationResourcesId(@ContainerPosition int position) { + return switch (position) { + case CONTAINER_POSITION_LEFT -> R.anim.overlay_task_fragment_close_to_left; + case CONTAINER_POSITION_TOP -> R.anim.overlay_task_fragment_close_to_top; + case CONTAINER_POSITION_RIGHT -> R.anim.overlay_task_fragment_close_to_right; + case CONTAINER_POSITION_BOTTOM -> R.anim.overlay_task_fragment_close_to_bottom; + default -> { + Log.w(TAG, "Unknown position:" + position); + yield Resources.ID_NULL; + } + }; + } + /** * Returns the expanded bounds if the {@code relBounds} violate minimum dimension or are not * fully covered by the task bounds. Otherwise, returns {@code relBounds}. @@ -757,7 +893,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { void expandTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { super.expandTaskFragment(wct, container); - mController.updateDivider(wct, container.getTaskContainer()); + mController.updateDivider( + wct, container.getTaskContainer(), false /* isTaskFragmentVanished */); } static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) { @@ -1248,4 +1385,16 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { return new ParentContainerInfo(taskProperties.getTaskMetrics(), configuration, windowLayoutInfo); } + + @VisibleForTesting + @NonNull + static String positionToString(@ContainerPosition int position) { + return switch (position) { + case CONTAINER_POSITION_LEFT -> "left"; + case CONTAINER_POSITION_TOP -> "top"; + case CONTAINER_POSITION_RIGHT -> "right"; + case CONTAINER_POSITION_BOTTOM -> "bottom"; + default -> "Unknown position:" + position; + }; + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index bf7ee27eddd2..dcc2d93060c9 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -16,7 +16,6 @@ package androidx.window.extensions.embedding; -import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; @@ -32,6 +31,7 @@ import android.app.WindowConfiguration.WindowingMode; import android.content.res.Configuration; import android.graphics.Rect; import android.os.IBinder; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.DisplayMetrics; import android.util.Log; @@ -48,6 +48,8 @@ import androidx.window.extensions.embedding.SplitAttributes.SplitType; import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType; import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType; +import com.android.window.flags.Flags; + import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -56,8 +58,9 @@ import java.util.Set; class TaskContainer { private static final String TAG = TaskContainer.class.getSimpleName(); - /** The unique task id. */ - private final int mTaskId; + /** Parcelable data of this TaskContainer. */ + @NonNull + private final ParcelableTaskContainerData mParcelableTaskContainerData; /** Active TaskFragments in this Task. */ @NonNull @@ -80,6 +83,9 @@ class TaskContainer { @NonNull private TaskFragmentParentInfo mInfo; + @NonNull + private SplitController mSplitController; + /** * TaskFragments that the organizer has requested to be closed. They should be removed when * the organizer receives @@ -116,30 +122,77 @@ class TaskContainer { /** * The {@link TaskContainer} constructor * - * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with - * {@code activityInTask}. - * @param activityInTask The {@link Activity} in the Task with {@code taskId}. It is used to - * initialize the {@link TaskContainer} properties. + * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with + * {@code activityInTask}. + * @param activityInTask The {@link Activity} in the Task with {@code taskId}. It is used to + * initialize the {@link TaskContainer} properties. + * @param splitController The {@link SplitController}. */ - TaskContainer(int taskId, @NonNull Activity activityInTask) { - if (taskId == INVALID_TASK_ID) { - throw new IllegalArgumentException("Invalid Task id"); - } - mTaskId = taskId; + TaskContainer(int taskId, @NonNull Activity activityInTask, + @NonNull SplitController splitController) { + mParcelableTaskContainerData = new ParcelableTaskContainerData(taskId, this); + final TaskProperties taskProperties = TaskProperties .getTaskPropertiesFromActivity(activityInTask); mInfo = new TaskFragmentParentInfo( taskProperties.getConfiguration(), taskProperties.getDisplayId(), + taskId, // Note that it is always called when there's a new Activity is started, which // implies the host task is visible and has an activity in the task. true /* visible */, true /* hasDirectActivity */, null /* decorSurface */); + mSplitController = splitController; + } + + /** This is only used when restoring it from a {@link ParcelableTaskContainerData}. */ + TaskContainer(@NonNull ParcelableTaskContainerData data, + @NonNull SplitController splitController, + @NonNull ArrayMap<IBinder, TaskFragmentInfo> taskFragmentInfoMap) { + mParcelableTaskContainerData = new ParcelableTaskContainerData(data, this); + mInfo = new TaskFragmentParentInfo(new Configuration(), 0 /* displayId */, -1 /* taskId */, + false /* visible */, false /* hasDirectActivity */, null /* decorSurface */); + mSplitController = splitController; + for (ParcelableTaskFragmentContainerData tfData : + data.getParcelableTaskFragmentContainerDataList()) { + final TaskFragmentInfo info = taskFragmentInfoMap.get(tfData.mToken); + if (info != null && !info.isEmpty()) { + final TaskFragmentContainer container = + new TaskFragmentContainer(tfData, splitController, this); + container.setInfo(new WindowContainerTransaction(), info); + mContainers.add(container); + } else { + Log.d(TAG, "Drop " + tfData + " while restoring Task " + data.mTaskId); + } + } + } + + @NonNull + ParcelableTaskContainerData getParcelableData() { + return mParcelableTaskContainerData; + } + + @NonNull + List<ParcelableTaskFragmentContainerData> getParcelableTaskFragmentContainerDataList() { + final List<ParcelableTaskFragmentContainerData> data = new ArrayList<>(mContainers.size()); + for (TaskFragmentContainer container : mContainers) { + data.add(container.getParcelableData()); + } + return data; + } + + @NonNull + List<ParcelableSplitContainerData> getParcelableSplitContainerDataList() { + final List<ParcelableSplitContainerData> data = new ArrayList<>(mSplitContainers.size()); + for (SplitContainer splitContainer : mSplitContainers) { + data.add(splitContainer.getParcelableData()); + } + return data; } int getTaskId() { - return mTaskId; + return mParcelableTaskContainerData.mTaskId; } int getDisplayId() { @@ -152,7 +205,8 @@ class TaskContainer { void setInvisible() { mInfo = new TaskFragmentParentInfo(mInfo.getConfiguration(), mInfo.getDisplayId(), - false /* visible */, mInfo.hasDirectActivity(), mInfo.getDecorSurface()); + mInfo.getTaskId(), false /* visible */, mInfo.hasDirectActivity(), + mInfo.getDecorSurface()); } boolean hasDirectActivity() { @@ -579,6 +633,12 @@ class TaskContainer { // Update overlay container after split pin container since the overlay should be on top of // pin container. updateAlwaysOnTopOverlayIfNecessary(); + + // TODO(b/289875940): Making backup-restore as an opt-in solution, before the flag goes + // to next-food. + if (Flags.aeBackStackRestore()) { + mSplitController.scheduleBackup(); + } } private void updateAlwaysOnTopOverlayIfNecessary() { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index 7173b0c95230..dc1d983997c6 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -36,7 +36,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; -import com.android.window.flags.Flags; import java.util.ArrayList; import java.util.Collections; @@ -54,14 +53,12 @@ import java.util.Objects; class TaskFragmentContainer { private static final int APPEAR_EMPTY_TIMEOUT_MS = 3000; + /** Parcelable data of this TaskFragmentContainer. */ @NonNull - private final SplitController mController; + private final ParcelableTaskFragmentContainerData mParcelableData; - /** - * Client-created token that uniquely identifies the task fragment container instance. - */ @NonNull - private final IBinder mToken; + private final SplitController mController; /** Parent leaf Task. */ @NonNull @@ -104,9 +101,6 @@ class TaskFragmentContainer { */ private final List<IBinder> mActivitiesToFinishOnExit = new ArrayList<>(); - @Nullable - private final String mOverlayTag; - /** * The launch options that was used to create this container. Must not {@link Bundle#isEmpty()} * for {@link #isOverlay()} container. @@ -114,29 +108,13 @@ class TaskFragmentContainer { @NonNull private final Bundle mLaunchOptions = new Bundle(); - /** - * The associated {@link Activity#getActivityToken()} of the overlay container. - * Must be {@code null} for non-overlay container. - * <p> - * If an overlay container is associated with an activity, this overlay container will be - * dismissed when the associated activity is destroyed. If the overlay container is visible, - * activity will be launched on top of the overlay container and expanded to fill the parent - * container. - */ - @Nullable - private final IBinder mAssociatedActivityToken; - /** 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(); - - /** * Windowing mode that was requested last via {@link android.window.WindowContainerTransaction}. */ + // TODO(b/289875940): review this and other field that might need to be moved in the base class. @WindowingMode private int mLastRequestedWindowingMode = WINDOWING_MODE_UNDEFINED; @@ -209,17 +187,17 @@ class TaskFragmentContainer { @NonNull SplitController controller, @Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag, @Nullable Bundle launchOptions, @Nullable Activity associatedActivity) { + mParcelableData = new ParcelableTaskFragmentContainerData( + new Binder("TaskFragmentContainer"), overlayTag, + associatedActivity != null ? associatedActivity.getActivityToken() : null); + if ((pendingAppearedActivity == null && pendingAppearedIntent == null) || (pendingAppearedActivity != null && pendingAppearedIntent != null)) { throw new IllegalArgumentException( "One and only one of pending activity and intent must be non-null"); } mController = controller; - mToken = new Binder("TaskFragmentContainer"); mTaskContainer = taskContainer; - mOverlayTag = overlayTag; - mAssociatedActivityToken = associatedActivity != null - ? associatedActivity.getActivityToken() : null; if (launchOptions != null) { mLaunchOptions.putAll(launchOptions); @@ -257,21 +235,29 @@ class TaskFragmentContainer { mPendingAppearedIntent = pendingAppearedIntent; // Save the information necessary for restoring the overlay when needed. - if (Flags.fixPipRestoreToOverlay() && overlayTag != null && pendingAppearedIntent != null + if (overlayTag != null && pendingAppearedIntent != null && associatedActivity != null && !associatedActivity.isFinishing()) { final IBinder associatedActivityToken = associatedActivity.getActivityToken(); - final OverlayContainerRestoreParams params = new OverlayContainerRestoreParams(mToken, - launchOptions, pendingAppearedIntent); + final OverlayContainerRestoreParams params = new OverlayContainerRestoreParams( + mParcelableData.mToken, launchOptions, pendingAppearedIntent); mController.mOverlayRestoreParams.put(associatedActivityToken, params); } } + /** This is only used when restoring it from a {@link ParcelableTaskFragmentContainerData}. */ + TaskFragmentContainer(@NonNull ParcelableTaskFragmentContainerData data, + @NonNull SplitController splitController, @NonNull TaskContainer taskContainer) { + mParcelableData = data; + mController = splitController; + mTaskContainer = taskContainer; + } + /** * Returns the client-created token that uniquely identifies this container. */ @NonNull IBinder getTaskFragmentToken() { - return mToken; + return mParcelableData.mToken; } /** List of non-finishing activities that belong to this container and live in this process. */ @@ -340,6 +326,13 @@ class TaskFragmentContainer { return mInfo != null && mInfo.isVisible(); } + /** + * See {@link TaskFragmentInfo#isTopNonFinishingChild()} + */ + boolean isTopNonFinishingChild() { + return mInfo != null && mInfo.isTopNonFinishingChild(); + } + /** Whether the TaskFragment is in an intermediate state waiting for the server update.*/ boolean isInIntermediateState() { if (mInfo == null) { @@ -383,7 +376,8 @@ class TaskFragmentContainer { return null; } return new ActivityStack(activities, isEmpty(), - ActivityStack.Token.createFromBinder(mToken), mOverlayTag); + ActivityStack.Token.createFromBinder(mParcelableData.mToken), + mParcelableData.mOverlayTag); } /** Adds the activity that will be reparented to this container. */ @@ -407,7 +401,7 @@ class TaskFragmentContainer { final ActivityThread.ActivityClientRecord record = ActivityThread .currentActivityThread().getActivityClient(activityToken); if (record != null) { - record.mTaskFragmentToken = mToken; + record.mTaskFragmentToken = mParcelableData.mToken; } } @@ -463,7 +457,7 @@ class TaskFragmentContainer { if (!isOverlayWithActivityAssociation()) { return; } - if (mAssociatedActivityToken == activityToken) { + if (mParcelableData.mAssociatedActivityToken == activityToken) { // If the associated activity is destroyed, also finish this overlay container. mController.mPresenter.cleanupContainer(wct, this, false /* shouldFinishDependent */); } @@ -770,8 +764,8 @@ class TaskFragmentContainer { * @see WindowContainerTransaction#setRelativeBounds */ boolean areLastRequestedBoundsEqual(@Nullable Rect relBounds) { - return (relBounds == null && mLastRequestedBounds.isEmpty()) - || mLastRequestedBounds.equals(relBounds); + return (relBounds == null && mParcelableData.mLastRequestedBounds.isEmpty()) + || mParcelableData.mLastRequestedBounds.equals(relBounds); } /** @@ -781,14 +775,14 @@ class TaskFragmentContainer { */ void setLastRequestedBounds(@Nullable Rect relBounds) { if (relBounds == null) { - mLastRequestedBounds.setEmpty(); + mParcelableData.mLastRequestedBounds.setEmpty(); } else { - mLastRequestedBounds.set(relBounds); + mParcelableData.mLastRequestedBounds.set(relBounds); } } @NonNull Rect getLastRequestedBounds() { - return mLastRequestedBounds; + return mParcelableData.mLastRequestedBounds; } /** @@ -959,6 +953,16 @@ class TaskFragmentContainer { return mTaskContainer.getTaskId(); } + @NonNull + IBinder getToken() { + return mParcelableData.mToken; + } + + @NonNull + ParcelableTaskFragmentContainerData getParcelableData() { + return mParcelableData; + } + /** Gets the parent Task. */ @NonNull TaskContainer getTaskContainer() { @@ -1005,7 +1009,7 @@ class TaskFragmentContainer { /** Returns whether this taskFragment container is an overlay container. */ boolean isOverlay() { - return mOverlayTag != null; + return mParcelableData.mOverlayTag != null; } /** @@ -1014,7 +1018,7 @@ class TaskFragmentContainer { */ @Nullable String getOverlayTag() { - return mOverlayTag; + return mParcelableData.mOverlayTag; } /** @@ -1039,7 +1043,7 @@ class TaskFragmentContainer { */ @Nullable IBinder getAssociatedActivityToken() { - return mAssociatedActivityToken; + return mParcelableData.mAssociatedActivityToken; } /** @@ -1047,11 +1051,11 @@ class TaskFragmentContainer { * a non-fill-parent overlay without activity association. */ boolean isAlwaysOnTopOverlay() { - return isOverlay() && mAssociatedActivityToken == null; + return isOverlay() && mParcelableData.mAssociatedActivityToken == null; } boolean isOverlayWithActivityAssociation() { - return isOverlay() && mAssociatedActivityToken != null; + return isOverlay() && mParcelableData.mAssociatedActivityToken != null; } @Override @@ -1068,13 +1072,13 @@ class TaskFragmentContainer { private String toString(boolean includeContainersToFinishOnExit) { return "TaskFragmentContainer{" + " parentTaskId=" + getTaskId() - + " token=" + mToken + + " token=" + mParcelableData.mToken + " topNonFinishingActivity=" + getTopNonFinishingActivity() + " runningActivityCount=" + getRunningActivityCount() + " isFinished=" + mIsFinished - + " overlayTag=" + mOverlayTag - + " associatedActivityToken=" + mAssociatedActivityToken - + " lastRequestedBounds=" + mLastRequestedBounds + + " overlayTag=" + mParcelableData.mOverlayTag + + " associatedActivityToken=" + mParcelableData.mAssociatedActivityToken + + " lastRequestedBounds=" + mParcelableData.mLastRequestedBounds + " pendingAppearedActivities=" + mPendingAppearedActivities + (includeContainersToFinishOnExit ? " containersToFinishOnExit=" + containersToFinishOnExitToString() : "") @@ -1197,7 +1201,7 @@ class TaskFragmentContainer { if (taskContainer == null) { // Adding a TaskContainer if no existed one. - taskContainer = new TaskContainer(mTaskId, mActivityInTask); + taskContainer = new TaskContainer(mTaskId, mActivityInTask, mSplitController); mSplitController.addTaskContainer(mTaskId, taskContainer); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/DisplayFoldFeatureUtil.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/DisplayFoldFeatureUtil.java index a0f481a911ad..870c92e6fdac 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/DisplayFoldFeatureUtil.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/DisplayFoldFeatureUtil.java @@ -16,48 +16,24 @@ package androidx.window.extensions.layout; -import androidx.window.common.CommonFoldingFeature; -import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; - -import java.util.ArrayList; -import java.util.List; +import androidx.window.common.layout.DisplayFoldFeatureCommon; /** * Util functions for working with {@link androidx.window.extensions.layout.DisplayFoldFeature}. */ -public class DisplayFoldFeatureUtil { +public final class DisplayFoldFeatureUtil { private DisplayFoldFeatureUtil() {} - private static DisplayFoldFeature create(CommonFoldingFeature foldingFeature, - boolean isHalfOpenedSupported) { - final int foldType; - if (foldingFeature.getType() == CommonFoldingFeature.COMMON_TYPE_HINGE) { - foldType = DisplayFoldFeature.TYPE_HINGE; - } else { - foldType = DisplayFoldFeature.TYPE_SCREEN_FOLD_IN; - } - DisplayFoldFeature.Builder featureBuilder = new DisplayFoldFeature.Builder(foldType); - - if (isHalfOpenedSupported) { - featureBuilder.addProperty(DisplayFoldFeature.FOLD_PROPERTY_SUPPORTS_HALF_OPENED); - } - return featureBuilder.build(); - } - /** - * Returns the list of supported {@link DisplayFeature} calculated from the - * {@link DeviceStateManagerFoldingFeatureProducer}. + * Returns a {@link DisplayFoldFeature} that matches the given {@link DisplayFoldFeatureCommon}. */ - public static List<DisplayFoldFeature> extractDisplayFoldFeatures( - DeviceStateManagerFoldingFeatureProducer producer) { - List<DisplayFoldFeature> foldFeatures = new ArrayList<>(); - List<CommonFoldingFeature> folds = producer.getFoldsWithUnknownState(); - - final boolean isHalfOpenedSupported = producer.isHalfOpenedSupported(); - for (CommonFoldingFeature fold : folds) { - foldFeatures.add(DisplayFoldFeatureUtil.create(fold, isHalfOpenedSupported)); + public static DisplayFoldFeature translate(DisplayFoldFeatureCommon foldFeatureCommon) { + final DisplayFoldFeature.Builder builder = + new DisplayFoldFeature.Builder(foldFeatureCommon.getType()); + for (int property: foldFeatureCommon.getProperties()) { + builder.addProperty(property); } - return foldFeatures; + return builder.build(); } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java index 070fa5bcfae4..f1ea19a60f97 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -17,12 +17,13 @@ package androidx.window.extensions.layout; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.INVALID_DISPLAY; -import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_FLAT; -import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_HALF_OPENED; -import static androidx.window.util.ExtensionHelper.isZero; -import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation; -import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect; +import static androidx.window.common.ExtensionHelper.isZero; +import static androidx.window.common.ExtensionHelper.rotateRectToDisplayRotation; +import static androidx.window.common.ExtensionHelper.transformToWindowSpaceRect; +import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_FLAT; +import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_HALF_OPENED; import android.app.Activity; import android.app.ActivityThread; @@ -30,10 +31,12 @@ import android.app.Application; import android.app.WindowConfiguration; import android.content.ComponentCallbacks; import android.content.Context; +import android.content.ContextWrapper; import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; +import android.os.StrictMode; import android.util.ArrayMap; import android.util.Log; @@ -41,9 +44,11 @@ import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiContext; -import androidx.window.common.CommonFoldingFeature; +import androidx.annotation.VisibleForTesting; import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.common.EmptyLifecycleCallbacksAdapter; +import androidx.window.common.collections.ListUtil; +import androidx.window.common.layout.CommonFoldingFeature; import androidx.window.extensions.core.util.function.Consumer; import androidx.window.extensions.util.DeduplicateConsumer; @@ -91,8 +96,8 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged()); mFoldingFeatureProducer = foldingFeatureProducer; mFoldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); - final List<DisplayFoldFeature> displayFoldFeatures = - DisplayFoldFeatureUtil.extractDisplayFoldFeatures(mFoldingFeatureProducer); + final List<DisplayFoldFeature> displayFoldFeatures = ListUtil.map( + mFoldingFeatureProducer.getDisplayFeatures(), DisplayFoldFeatureUtil::translate); mSupportedWindowFeatures = new SupportedWindowFeatures.Builder(displayFoldFeatures).build(); } @@ -134,10 +139,23 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { || containsConsumer(consumer)) { return; } + final IllegalArgumentException exception = new IllegalArgumentException( + "Context must be a UI Context with display association, which should be" + + " an Activity, WindowContext or InputMethodService"); if (!context.isUiContext()) { - throw new IllegalArgumentException("Context must be a UI Context, which should be" - + " an Activity, WindowContext or InputMethodService"); + throw exception; } + if (context.getAssociatedDisplayId() == INVALID_DISPLAY) { + // This is to identify if #isUiContext of a non-UI Context is overridden. + // #isUiContext is more likely to be overridden than #getAssociatedDisplayId + // since #isUiContext is a public API. + StrictMode.onIncorrectContextUsed("The registered Context is a UI Context " + + "but not associated with any display. " + + "This Context may not receive any WindowLayoutInfo update. " + + dumpAllBaseContextToString(context), exception); + } + Log.d(TAG, "Register WindowLayoutInfoListener on " + + dumpAllBaseContextToString(context)); mFoldingFeatureProducer.getData((features) -> { WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, features); consumer.accept(newWindowLayout); @@ -156,6 +174,16 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } } + @NonNull + private String dumpAllBaseContextToString(@NonNull Context context) { + final StringBuilder builder = new StringBuilder("Context=" + context); + while ((context instanceof ContextWrapper wrapper) && wrapper.getBaseContext() != null) { + context = wrapper.getBaseContext(); + builder.append(", of which baseContext=").append(context); + } + return builder.toString(); + } + @Override public void removeWindowLayoutInfoListener( @NonNull java.util.function.Consumer<WindowLayoutInfo> consumer) { @@ -257,7 +285,8 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { } } - private void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) { + @VisibleForTesting + void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) { synchronized (mLock) { mLastReportedFoldingFeatures.clear(); mLastReportedFoldingFeatures.addAll(storedFeatures); @@ -409,7 +438,18 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { * @return true if the display features should be reported for the UI Context, false otherwise. */ private boolean shouldReportDisplayFeatures(@NonNull @UiContext Context context) { - int displayId = context.getDisplay().getDisplayId(); + int displayId = context.getAssociatedDisplayId(); + if (!context.isUiContext() || displayId == INVALID_DISPLAY) { + // This could happen if a caller sets MutableContextWrapper's base Context to a non-UI + // Context. + StrictMode.onIncorrectContextUsed("Context is not a UI Context anymore." + + " Was the base context changed? It's suggested to unregister" + + " the windowLayoutInfo callback before changing the base Context." + + " UI Contexts are Activity, InputMethodService or context created" + + " with createWindowContext. " + dumpAllBaseContextToString(context), + new UnsupportedOperationException("Context is not a UI Context anymore." + + " Was the base context changed?")); + } if (displayId != DEFAULT_DISPLAY) { // Display features are not supported on secondary displays. return false; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java index bb6ab47b144d..6e0e7115cfb1 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java @@ -17,8 +17,8 @@ package androidx.window.sidecar; import static android.view.Display.DEFAULT_DISPLAY; -import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation; -import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect; +import static androidx.window.common.ExtensionHelper.rotateRectToDisplayRotation; +import static androidx.window.common.ExtensionHelper.transformToWindowSpaceRect; import android.annotation.NonNull; import android.app.Activity; @@ -26,7 +26,7 @@ import android.app.ActivityThread; import android.graphics.Rect; import android.os.IBinder; -import androidx.window.common.CommonFoldingFeature; +import androidx.window.common.layout.CommonFoldingFeature; import java.util.ArrayList; import java.util.Collections; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarImpl.java index 339908a3a9a4..a1de2062e906 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 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,6 +16,7 @@ package androidx.window.sidecar; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; import android.app.Application; @@ -23,27 +24,38 @@ import android.content.Context; import android.hardware.devicestate.DeviceStateManager; import android.os.Bundle; import android.os.IBinder; +import android.util.ArraySet; +import android.util.Log; -import androidx.annotation.NonNull; -import androidx.window.common.CommonFoldingFeature; +import androidx.window.common.BaseDataProducer; import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.common.EmptyLifecycleCallbacksAdapter; import androidx.window.common.RawFoldingFeatureProducer; -import androidx.window.util.BaseDataProducer; +import androidx.window.common.layout.CommonFoldingFeature; import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.Set; /** - * Reference implementation of androidx.window.sidecar OEM interface for use with - * WindowManager Jetpack. + * Basic implementation of the {@link SidecarInterface}. An OEM can choose to use it as the base + * class for their implementation. */ -class SampleSidecarImpl extends StubSidecar { +class SidecarImpl implements SidecarInterface { + + private static final String TAG = "WindowManagerSidecar"; + + @Nullable + private SidecarCallback mSidecarCallback; + private final ArraySet<IBinder> mWindowLayoutChangeListenerTokens = new ArraySet<>(); + private boolean mDeviceStateChangeListenerRegistered; + @NonNull private List<CommonFoldingFeature> mStoredFeatures = new ArrayList<>(); - SampleSidecarImpl(Context context) { + SidecarImpl(Context context) { ((Application) context.getApplicationContext()) - .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged()); + .registerActivityLifecycleCallbacks(new SidecarImpl.NotifyOnConfigurationChanged()); RawFoldingFeatureProducer settingsFeatureProducer = new RawFoldingFeatureProducer(context); BaseDataProducer<List<CommonFoldingFeature>> foldingFeatureProducer = new DeviceStateManagerFoldingFeatureProducer(context, @@ -53,11 +65,46 @@ class SampleSidecarImpl extends StubSidecar { foldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); } - private void setStoredFeatures(List<CommonFoldingFeature> storedFeatures) { - mStoredFeatures = storedFeatures; + @NonNull + @Override + public SidecarDeviceState getDeviceState() { + return SidecarHelper.calculateDeviceState(mStoredFeatures); + } + + @NonNull + @Override + public SidecarWindowLayoutInfo getWindowLayoutInfo(@NonNull IBinder windowToken) { + return SidecarHelper.calculateWindowLayoutInfo(windowToken, mStoredFeatures); + } + + @Override + public void setSidecarCallback(@NonNull SidecarCallback sidecarCallback) { + mSidecarCallback = sidecarCallback; + } + + @Override + public void onWindowLayoutChangeListenerAdded(@NonNull IBinder iBinder) { + mWindowLayoutChangeListenerTokens.add(iBinder); + onListenersChanged(); + } + + @Override + public void onWindowLayoutChangeListenerRemoved(@NonNull IBinder iBinder) { + mWindowLayoutChangeListenerTokens.remove(iBinder); + onListenersChanged(); + } + + @Override + public void onDeviceStateListenersChanged(boolean isEmpty) { + mDeviceStateChangeListenerRegistered = !isEmpty; + onListenersChanged(); + } + + private void setStoredFeatures(@NonNull List<CommonFoldingFeature> storedFeatures) { + mStoredFeatures = Objects.requireNonNull(storedFeatures); } - private void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) { + private void onDisplayFeaturesChanged(@NonNull List<CommonFoldingFeature> storedFeatures) { setStoredFeatures(storedFeatures); updateDeviceState(getDeviceState()); for (IBinder windowToken : getWindowsListeningForLayoutChanges()) { @@ -66,25 +113,43 @@ class SampleSidecarImpl extends StubSidecar { } } - @NonNull - @Override - public SidecarDeviceState getDeviceState() { - return SidecarHelper.calculateDeviceState(mStoredFeatures); + void updateDeviceState(@NonNull SidecarDeviceState newState) { + if (mSidecarCallback != null) { + try { + mSidecarCallback.onDeviceStateChanged(newState); + } catch (AbstractMethodError e) { + Log.e(TAG, "App is using an outdated Window Jetpack library", e); + } + } + } + + void updateWindowLayout(@NonNull IBinder windowToken, + @NonNull SidecarWindowLayoutInfo newLayout) { + if (mSidecarCallback != null) { + try { + mSidecarCallback.onWindowLayoutChanged(windowToken, newLayout); + } catch (AbstractMethodError e) { + Log.e(TAG, "App is using an outdated Window Jetpack library", e); + } + } } @NonNull - @Override - public SidecarWindowLayoutInfo getWindowLayoutInfo(@NonNull IBinder windowToken) { - return SidecarHelper.calculateWindowLayoutInfo(windowToken, mStoredFeatures); + private Set<IBinder> getWindowsListeningForLayoutChanges() { + return mWindowLayoutChangeListenerTokens; } - @Override - protected void onListenersChanged() { + protected boolean hasListeners() { + return !mWindowLayoutChangeListenerTokens.isEmpty() || mDeviceStateChangeListenerRegistered; + } + + private void onListenersChanged() { if (hasListeners()) { onDisplayFeaturesChanged(mStoredFeatures); } } + private final class NotifyOnConfigurationChanged extends EmptyLifecycleCallbacksAdapter { @Override public void onActivityCreated(@NonNull Activity activity, diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java index 686a31b6be04..1e306fcebda0 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java @@ -36,7 +36,7 @@ public class SidecarProvider { @Nullable public static SidecarInterface getSidecarImpl(Context context) { return isWindowExtensionsEnabled() - ? new SampleSidecarImpl(context.getApplicationContext()) + ? new SidecarImpl(context.getApplicationContext()) : null; } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/StubSidecar.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/StubSidecar.java deleted file mode 100644 index 46c1f3ba4691..000000000000 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/StubSidecar.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.window.sidecar; - -import android.os.IBinder; -import android.util.Log; - -import androidx.annotation.NonNull; - -import java.util.HashSet; -import java.util.Set; - -/** - * Basic implementation of the {@link SidecarInterface}. An OEM can choose to use it as the base - * class for their implementation. - */ -abstract class StubSidecar implements SidecarInterface { - - private static final String TAG = "WindowManagerSidecar"; - - private SidecarCallback mSidecarCallback; - final Set<IBinder> mWindowLayoutChangeListenerTokens = new HashSet<>(); - private boolean mDeviceStateChangeListenerRegistered; - - StubSidecar() { - } - - @Override - public void setSidecarCallback(@NonNull SidecarCallback sidecarCallback) { - this.mSidecarCallback = sidecarCallback; - } - - @Override - public void onWindowLayoutChangeListenerAdded(@NonNull IBinder iBinder) { - this.mWindowLayoutChangeListenerTokens.add(iBinder); - this.onListenersChanged(); - } - - @Override - public void onWindowLayoutChangeListenerRemoved(@NonNull IBinder iBinder) { - this.mWindowLayoutChangeListenerTokens.remove(iBinder); - this.onListenersChanged(); - } - - @Override - public void onDeviceStateListenersChanged(boolean isEmpty) { - this.mDeviceStateChangeListenerRegistered = !isEmpty; - this.onListenersChanged(); - } - - void updateDeviceState(SidecarDeviceState newState) { - if (this.mSidecarCallback != null) { - try { - mSidecarCallback.onDeviceStateChanged(newState); - } catch (AbstractMethodError e) { - Log.e(TAG, "App is using an outdated Window Jetpack library", e); - } - } - } - - void updateWindowLayout(@NonNull IBinder windowToken, - @NonNull SidecarWindowLayoutInfo newLayout) { - if (this.mSidecarCallback != null) { - try { - mSidecarCallback.onWindowLayoutChanged(windowToken, newLayout); - } catch (AbstractMethodError e) { - Log.e(TAG, "App is using an outdated Window Jetpack library", e); - } - } - } - - @NonNull - Set<IBinder> getWindowsListeningForLayoutChanges() { - return mWindowLayoutChangeListenerTokens; - } - - protected boolean hasListeners() { - return !mWindowLayoutChangeListenerTokens.isEmpty() || mDeviceStateChangeListenerRegistered; - } - - protected abstract void onListenersChanged(); -} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java deleted file mode 100644 index ec301dc34aaa..000000000000 --- a/libs/WindowManager/Jetpack/src/androidx/window/util/DataProducer.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.window.util; - -import android.annotation.NonNull; - -import java.util.function.Consumer; - -/** - * Produces data through {@link DataProducer#getData} and provides a mechanism for receiving - * a callback when the data managed by the produces has changed. - * - * @param <T> The type of data this producer returns through {@link DataProducer#getData}. - */ -public interface DataProducer<T> { - /** - * Emits the first available data at that point in time. - * @param dataConsumer a {@link Consumer} that will receive one value. - */ - void getData(@NonNull Consumer<T> dataConsumer); - - /** - * Adds a callback to be notified when the data returned - * from {@link DataProducer#getData} has changed. - */ - void addDataChangedCallback(@NonNull Consumer<T> callback); - - /** Removes a callback previously added with {@link #addDataChangedCallback(Consumer)}. */ - void removeDataChangedCallback(@NonNull Consumer<T> callback); -} diff --git a/libs/WindowManager/Jetpack/tests/unittest/Android.bp b/libs/WindowManager/Jetpack/tests/unittest/Android.bp index 020da924c60d..bd430c0e610b 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/Android.bp +++ b/libs/WindowManager/Jetpack/tests/unittest/Android.bp @@ -32,6 +32,7 @@ android_test { ], static_libs: [ + "TestParameterInjector", "androidx.window.extensions", "androidx.window.extensions.core_core", "junit", diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/util/ExtensionHelperTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/ExtensionHelperTest.java index 3278cdf1c337..b6e951961a69 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/util/ExtensionHelperTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/ExtensionHelperTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.window.util; +package androidx.window.common; import static org.junit.Assert.assertEquals; diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/collections/ListUtilTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/collections/ListUtilTest.java new file mode 100644 index 000000000000..a077bdfef194 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/collections/ListUtilTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 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.common.collections; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * Test class for {@link ListUtil}. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:ListUtil + */ +public class ListUtilTest { + + @Test + public void test_map_empty_returns_empty() { + final List<String> emptyList = new ArrayList<>(); + final List<Integer> result = ListUtil.map(emptyList, String::length); + assertThat(result).isEmpty(); + } + + @Test + public void test_map_maintains_order() { + final List<String> source = new ArrayList<>(); + source.add("a"); + source.add("aa"); + + final List<Integer> result = ListUtil.map(source, String::length); + + assertThat(result).containsExactly(1, 2).inOrder(); + } +} diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/layout/DisplayFoldFeatureCommonTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/layout/DisplayFoldFeatureCommonTest.java new file mode 100644 index 000000000000..6c178511388b --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/layout/DisplayFoldFeatureCommonTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2024 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.common.layout; + +import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN; +import static androidx.window.common.layout.CommonFoldingFeature.COMMON_TYPE_FOLD; +import static androidx.window.common.layout.CommonFoldingFeature.COMMON_TYPE_HINGE; +import static androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED; +import static androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_TYPE_HINGE; +import static androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN; + +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Rect; +import android.util.ArraySet; + +import org.junit.Test; + +import java.util.Set; + +/** + * Test class for {@link DisplayFoldFeatureCommon}. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:DisplayFoldFeatureCommonTest + */ +public class DisplayFoldFeatureCommonTest { + + @Test + public void test_different_type_not_equals() { + final Set<Integer> properties = new ArraySet<>(); + final DisplayFoldFeatureCommon first = + new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, properties); + final DisplayFoldFeatureCommon second = + new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN, properties); + + assertThat(first).isEqualTo(second); + } + + @Test + public void test_different_property_set_not_equals() { + final Set<Integer> firstProperties = new ArraySet<>(); + final Set<Integer> secondProperties = new ArraySet<>(); + secondProperties.add(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED); + final DisplayFoldFeatureCommon first = + new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, firstProperties); + final DisplayFoldFeatureCommon second = + new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, secondProperties); + + assertThat(first).isNotEqualTo(second); + } + + @Test + public void test_check_single_property_exists() { + final Set<Integer> properties = new ArraySet<>(); + properties.add(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED); + final DisplayFoldFeatureCommon foldFeatureCommon = + new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, properties); + + assertThat( + foldFeatureCommon.hasProperty(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED)) + .isTrue(); + } + + @Test + public void test_check_multiple_properties_exists() { + final Set<Integer> properties = new ArraySet<>(); + properties.add(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED); + final DisplayFoldFeatureCommon foldFeatureCommon = + new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, properties); + + assertThat(foldFeatureCommon.hasProperties( + DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED)) + .isTrue(); + } + + @Test + public void test_properties_matches_getter() { + final Set<Integer> properties = new ArraySet<>(); + properties.add(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED); + final DisplayFoldFeatureCommon foldFeatureCommon = + new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, properties); + + assertThat(foldFeatureCommon.getProperties()).isEqualTo(properties); + } + + @Test + public void test_type_matches_getter() { + final Set<Integer> properties = new ArraySet<>(); + final DisplayFoldFeatureCommon foldFeatureCommon = + new DisplayFoldFeatureCommon(DISPLAY_FOLD_FEATURE_TYPE_HINGE, properties); + + assertThat(foldFeatureCommon.getType()).isEqualTo(DISPLAY_FOLD_FEATURE_TYPE_HINGE); + } + + @Test + public void test_create_half_opened_feature() { + final CommonFoldingFeature foldingFeature = + new CommonFoldingFeature(COMMON_TYPE_HINGE, COMMON_STATE_UNKNOWN, new Rect()); + final DisplayFoldFeatureCommon foldFeatureCommon = DisplayFoldFeatureCommon.create( + foldingFeature, true); + + assertThat(foldFeatureCommon.getType()).isEqualTo(DISPLAY_FOLD_FEATURE_TYPE_HINGE); + assertThat( + foldFeatureCommon.hasProperty(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED)) + .isTrue(); + } + + @Test + public void test_create_fold_feature_no_half_opened() { + final CommonFoldingFeature foldingFeature = + new CommonFoldingFeature(COMMON_TYPE_FOLD, COMMON_STATE_UNKNOWN, new Rect()); + final DisplayFoldFeatureCommon foldFeatureCommon = DisplayFoldFeatureCommon.create( + foldingFeature, true); + + assertThat(foldFeatureCommon.getType()).isEqualTo(DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN); + assertThat( + foldFeatureCommon.hasProperty(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED)) + .isTrue(); + } +} diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java index 4267749dfa6b..92f48141b607 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java @@ -16,7 +16,7 @@ package androidx.window.extensions; -import static androidx.window.extensions.WindowExtensionsImpl.EXTENSIONS_VERSION_CURRENT_PLATFORM; +import static androidx.window.extensions.WindowExtensionsImpl.getExtensionsVersionCurrentPlatform; import static com.google.common.truth.Truth.assertThat; @@ -29,6 +29,7 @@ import android.view.WindowManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.window.extensions.embedding.AnimationBackground; +import androidx.window.extensions.embedding.AnimationParams; import androidx.window.extensions.embedding.SplitAttributes; import org.junit.Before; @@ -58,7 +59,8 @@ public class WindowExtensionsTest { @Test public void testGetVendorApiLevel_extensionsEnabled_matchesCurrentVersion() { assumeTrue(WindowManager.hasWindowExtensionsEnabled()); - assertThat(mVersion).isEqualTo(EXTENSIONS_VERSION_CURRENT_PLATFORM); + assumeFalse(((WindowExtensionsImpl) mExtensions).hasLevelOverride()); + assertThat(mVersion).isEqualTo(getExtensionsVersionCurrentPlatform()); } @Test @@ -112,5 +114,13 @@ public class WindowExtensionsTest { .isEqualTo(new SplitAttributes.SplitType.RatioSplitType(0.5f)); assertThat(splitAttributes.getAnimationBackground()) .isEqualTo(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT); + assertThat(splitAttributes.getAnimationParams().getAnimationBackground()) + .isEqualTo(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT); + assertThat(splitAttributes.getAnimationParams().getOpenAnimationResId()) + .isEqualTo(AnimationParams.DEFAULT_ANIMATION_RESOURCES_ID); + assertThat(splitAttributes.getAnimationParams().getCloseAnimationResId()) + .isEqualTo(AnimationParams.DEFAULT_ANIMATION_RESOURCES_ID); + assertThat(splitAttributes.getAnimationParams().getChangeAnimationResId()) + .isEqualTo(AnimationParams.DEFAULT_ANIMATION_RESOURCES_ID); } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java index 4f51815ed05d..bc18cd289e05 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java @@ -82,6 +82,7 @@ import java.util.concurrent.Executor; */ @Presubmit @SmallTest +@SuppressWarnings("GuardedBy") @RunWith(AndroidJUnit4.class) public class DividerPresenterTest { @Rule @@ -186,7 +187,8 @@ public class DividerPresenterTest { mDividerPresenter.updateDivider( mTransaction, mParentInfo, - mSplitContainer); + mSplitContainer, + false /* isTaskFragmentVanished */); assertNotEquals(mProperties, mDividerPresenter.mProperties); verify(mRenderer).update(); @@ -206,7 +208,8 @@ public class DividerPresenterTest { mDividerPresenter.updateDivider( mTransaction, mParentInfo, - mSplitContainer); + mSplitContainer, + false /* isTaskFragmentVanished */); assertNotEquals(mProperties, mDividerPresenter.mProperties); verify(mRenderer).update(); @@ -222,7 +225,8 @@ public class DividerPresenterTest { mDividerPresenter.updateDivider( mTransaction, mParentInfo, - mSplitContainer); + mSplitContainer, + false /* isTaskFragmentVanished */); assertEquals(mProperties, mDividerPresenter.mProperties); verify(mRenderer, never()).update(); @@ -234,7 +238,42 @@ public class DividerPresenterTest { mDividerPresenter.updateDivider( mTransaction, mParentInfo, - null /* splitContainer */); + null /* splitContainer */, + false /* isTaskFragmentVanished */); + final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder( + OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE) + .build(); + + verify(mTransaction).addTaskFragmentOperation( + mPrimaryContainerToken, taskFragmentOperation); + verify(mRenderer).release(); + assertNull(mDividerPresenter.mRenderer); + assertNull(mDividerPresenter.mProperties); + assertNull(mDividerPresenter.mDecorSurfaceOwner); + } + + @Test + public void testUpdateDivider_noChangeWhenHasContainersToFinishButTaskFragmentNotVanished() { + mDividerPresenter.setHasContainersToFinish(true); + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + null /* splitContainer */, + false /* isTaskFragmentVanished */); + + assertEquals(mProperties, mDividerPresenter.mProperties); + verify(mRenderer, never()).update(); + verify(mTransaction, never()).addTaskFragmentOperation(any(), any()); + } + + @Test + public void testUpdateDivider_dividerRemovedWhenHasContainersToFinishAndTaskFragmentVanished() { + mDividerPresenter.setHasContainersToFinish(true); + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + null /* splitContainer */, + true /* isTaskFragmentVanished */); final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder( OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE) .build(); @@ -254,7 +293,8 @@ public class DividerPresenterTest { mDividerPresenter.updateDivider( mTransaction, mParentInfo, - mSplitContainer); + mSplitContainer, + false /* isTaskFragmentVanished */); final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder( OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE) .build(); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java index d649c6d57137..5c85778ee42d 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java @@ -163,12 +163,14 @@ public class EmbeddingTestUtils { } /** Creates a mock TaskFragmentInfo for the given TaskFragment. */ + @NonNull static TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container, @NonNull Activity activity) { return createMockTaskFragmentInfo(container, activity, true /* isVisible */); } /** Creates a mock TaskFragmentInfo for the given TaskFragment. */ + @NonNull static TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container, @NonNull Activity activity, boolean isVisible) { return new TaskFragmentInfo(container.getTaskFragmentToken(), @@ -182,7 +184,27 @@ public class EmbeddingTestUtils { false /* isTaskClearedForReuse */, false /* isTaskFragmentClearedForPip */, false /* isClearedForReorderActivityToFront */, - new Point()); + new Point(), + false /* isTopChild */); + } + + /** Creates a mock TaskFragmentInfo for the given TaskFragment. */ + @NonNull + static TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container, + @NonNull Activity activity, boolean isVisible, boolean isOnTop) { + return new TaskFragmentInfo(container.getTaskFragmentToken(), + mock(WindowContainerToken.class), + new Configuration(), + 1, + isVisible, + Collections.singletonList(activity.getActivityToken()), + new ArrayList<>(), + new Point(), + false /* isTaskClearedForReuse */, + false /* isTaskFragmentClearedForPip */, + false /* isClearedForReorderActivityToFront */, + new Point(), + isOnTop); } static ActivityInfo createActivityInfoWithMinDimensions() { @@ -200,7 +222,7 @@ public class EmbeddingTestUtils { doReturn(resources).when(activity).getResources(); doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId(); - return new TaskContainer(TASK_ID, activity); + return new TaskContainer(TASK_ID, activity, mock(SplitController.class)); } static TaskContainer createTestTaskContainer(@NonNull SplitController controller) { diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java index 7b473b04548c..ac004c301598 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java @@ -134,6 +134,7 @@ public class JetpackTaskFragmentOrganizerTest { mock(WindowContainerToken.class), new Configuration(), 0 /* runningActivityCount */, false /* isVisible */, new ArrayList<>(), new ArrayList<>(), new Point(), false /* isTaskClearedForReuse */, false /* isTaskFragmentClearedForPip */, - false /* isClearedForReorderActivityToFront */, new Point()); + false /* isClearedForReorderActivityToFront */, new Point(), + false /* isTopChild */); } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java index 7a0b9a0ece6b..5b97e7e2ca71 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java @@ -30,6 +30,13 @@ import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSpli import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPlaceholderRuleBuilder; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTfContainer; +import static androidx.window.extensions.embedding.SplitController.KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY; +import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM; +import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_LEFT; +import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT; +import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP; +import static androidx.window.extensions.embedding.SplitPresenter.getOverlayPosition; +import static androidx.window.extensions.embedding.SplitPresenter.positionToString; import static androidx.window.extensions.embedding.SplitPresenter.sanitizeBounds; import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK; @@ -73,7 +80,6 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.extensions.layout.WindowLayoutComponentImpl; @@ -81,11 +87,15 @@ import androidx.window.extensions.layout.WindowLayoutInfo; import com.android.window.flags.Flags; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -103,7 +113,7 @@ import java.util.List; @SuppressWarnings("GuardedBy") @Presubmit @SmallTest -@RunWith(AndroidJUnit4.class) +@RunWith(TestParameterInjector.class) public class OverlayPresentationTest { @Rule public MockitoRule rule = MockitoJUnit.rule(); @@ -259,7 +269,7 @@ public class OverlayPresentationTest { } @Test - public void testCreateOrUpdateOverlay_visibleOverlaySameTagInTask_dismissOverlay() { + public void testCreateOrUpdateOverlay_topOverlayInTask_dismissOverlay() { createExistingOverlayContainers(); final TaskFragmentContainer overlayContainer = @@ -287,26 +297,6 @@ public class OverlayPresentationTest { } @Test - public void testCreateOrUpdateOverlay_sameTagTaskAndActivity_updateOverlay() { - createExistingOverlayContainers(); - - final Rect bounds = new Rect(0, 0, 100, 100); - mSplitController.setActivityStackAttributesCalculator(params -> - new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build()); - final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded( - mOverlayContainer1.getOverlayTag()); - - assertWithMessage("overlayContainer1 must be updated since the new overlay container" - + " is launched with the same tag and task") - .that(mSplitController.getAllNonFinishingOverlayContainers()) - .containsExactly(mOverlayContainer1, mOverlayContainer2); - - assertThat(overlayContainer).isEqualTo(mOverlayContainer1); - verify(mSplitPresenter).resizeTaskFragment(eq(mTransaction), - eq(mOverlayContainer1.getTaskFragmentToken()), eq(bounds)); - } - - @Test public void testCreateOrUpdateOverlay_sameTagAndTaskButNotActivity_dismissOverlay() { createExistingOverlayContainers(); @@ -354,6 +344,43 @@ public class OverlayPresentationTest { } @Test + public void testCreateOrUpdateAlwaysOnTopOverlay_dismissMultipleOverlaysInTask() { + createExistingOverlayContainers(); + // Create another overlay in task. + final TaskFragmentContainer overlayContainer3 = + createTestOverlayContainer(TASK_ID, "test3"); + assertThat(mSplitController.getAllNonFinishingOverlayContainers()) + .containsExactly(mOverlayContainer1, mOverlayContainer2, overlayContainer3); + + final TaskFragmentContainer overlayContainer = + createOrUpdateAlwaysOnTopOverlay("test4"); + + assertWithMessage("overlayContainer1 and overlayContainer3 must be dismissed") + .that(mSplitController.getAllNonFinishingOverlayContainers()) + .containsExactly(mOverlayContainer2, overlayContainer); + } + + @Test + public void testCreateOrUpdateAlwaysOnTopOverlay_updateOverlay() { + createExistingOverlayContainers(); + // Create another overlay in task. + final TaskFragmentContainer alwaysOnTopOverlay = createTestOverlayContainer(TASK_ID, + "test3", true /* isVisible */, false /* associateLaunchingActivity */); + final ActivityStackAttributes attrs = new ActivityStackAttributes.Builder() + .setRelativeBounds(new Rect(0, 0, 100, 100)).build(); + mSplitController.setActivityStackAttributesCalculator(params -> attrs); + + Mockito.clearInvocations(mSplitPresenter); + final TaskFragmentContainer overlayContainer = + createOrUpdateAlwaysOnTopOverlay(alwaysOnTopOverlay.getOverlayTag()); + + assertWithMessage("overlayContainer1 and overlayContainer3 must be dismissed") + .that(mSplitController.getAllNonFinishingOverlayContainers()) + .containsExactly(mOverlayContainer2, alwaysOnTopOverlay); + assertThat(overlayContainer).isEqualTo(alwaysOnTopOverlay); + } + + @Test public void testCreateOrUpdateOverlay_launchFromSplit_returnNull() { final Activity primaryActivity = createMockActivity(); final Activity secondaryActivity = createMockActivity(); @@ -373,13 +400,13 @@ public class OverlayPresentationTest { } private void createExistingOverlayContainers() { - createExistingOverlayContainers(true /* visible */); + createExistingOverlayContainers(true /* isOnTop */); } - private void createExistingOverlayContainers(boolean visible) { - mOverlayContainer1 = createTestOverlayContainer(TASK_ID, "test1", visible, + private void createExistingOverlayContainers(boolean isOnTop) { + mOverlayContainer1 = createTestOverlayContainer(TASK_ID, "test1", isOnTop, true /* associatedLaunchingActivity */, mActivity); - mOverlayContainer2 = createTestOverlayContainer(TASK_ID + 1, "test2", visible); + mOverlayContainer2 = createTestOverlayContainer(TASK_ID + 1, "test2", isOnTop); List<TaskFragmentContainer> overlayContainers = mSplitController .getAllNonFinishingOverlayContainers(); assertThat(overlayContainers).containsExactly(mOverlayContainer1, mOverlayContainer2); @@ -522,7 +549,7 @@ public class OverlayPresentationTest { assertThat(taskContainer.getTaskFragmentContainers()).containsExactly(overlayContainer); taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(Configuration.EMPTY, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + DEFAULT_DISPLAY, TASK_ID, true /* visible */, false /* hasDirectActivity */, null /* decorSurface */)); mSplitController.updateOverlayContainer(mTransaction, overlayContainer); @@ -591,7 +618,8 @@ public class OverlayPresentationTest { final TaskContainer.TaskProperties taskProperties = taskContainer.getTaskProperties(); final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo( new Configuration(taskProperties.getConfiguration()), taskProperties.getDisplayId(), - true /* visible */, false /* hasDirectActivity */, null /* decorSurface */); + TASK_ID, true /* visible */, false /* hasDirectActivity */, + null /* decorSurface */); parentInfo.getConfiguration().windowConfiguration.getBounds().offset(10, 10); mSplitController.onTaskFragmentParentInfoChanged(mTransaction, TASK_ID, parentInfo); @@ -615,7 +643,8 @@ public class OverlayPresentationTest { final TaskContainer.TaskProperties taskProperties = taskContainer.getTaskProperties(); final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo( new Configuration(taskProperties.getConfiguration()), taskProperties.getDisplayId(), - true /* visible */, false /* hasDirectActivity */, null /* decorSurface */); + TASK_ID, true /* visible */, false /* hasDirectActivity */, + null /* decorSurface */); mSplitController.onTaskFragmentParentInfoChanged(mTransaction, TASK_ID, parentInfo); @@ -666,8 +695,8 @@ public class OverlayPresentationTest { attributes.getRelativeBounds()); verify(mSplitPresenter).updateTaskFragmentWindowingModeIfRegistered(mTransaction, container, WINDOWING_MODE_MULTI_WINDOW); - verify(mSplitPresenter).updateAnimationParams(mTransaction, token, - TaskFragmentAnimationParams.DEFAULT); + verify(mSplitPresenter).updateAnimationParams(eq(mTransaction), eq(token), + any(TaskFragmentAnimationParams.class)); verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, container, true); verify(mSplitPresenter, never()).setTaskFragmentPinned(any(), any(TaskFragmentContainer.class), anyBoolean()); @@ -691,8 +720,8 @@ public class OverlayPresentationTest { attributes.getRelativeBounds()); verify(mSplitPresenter).updateTaskFragmentWindowingModeIfRegistered(mTransaction, container, WINDOWING_MODE_MULTI_WINDOW); - verify(mSplitPresenter).updateAnimationParams(mTransaction, token, - TaskFragmentAnimationParams.DEFAULT); + verify(mSplitPresenter).updateAnimationParams(eq(mTransaction), eq(token), + any(TaskFragmentAnimationParams.class)); verify(mSplitPresenter, never()).setTaskFragmentIsolatedNavigation(any(), any(TaskFragmentContainer.class), anyBoolean()); verify(mSplitPresenter).setTaskFragmentPinned(mTransaction, container, true); @@ -847,8 +876,6 @@ public class OverlayPresentationTest { @Test public void testOnActivityReparentedToTask_overlayRestoration() { - mSetFlagRule.enableFlags(Flags.FLAG_FIX_PIP_RESTORE_TO_OVERLAY); - // Prepares and mock the data necessary for the test. final IBinder activityToken = mActivity.getActivityToken(); final Intent intent = new Intent(); @@ -870,6 +897,72 @@ public class OverlayPresentationTest { eq(overlayContainer.getTaskFragmentToken()), eq(activityToken)); } + @Test + public void testGetOverlayPosition(@TestParameter OverlayPositionTestParams params) { + final Rect taskBounds = new Rect(TASK_BOUNDS); + final Rect overlayBounds = params.toOverlayBounds(); + final int overlayPosition = getOverlayPosition(overlayBounds, taskBounds); + + assertWithMessage("The overlay position must be " + + positionToString(params.mPosition) + ", but is " + + positionToString(overlayPosition) + + ", parent bounds=" + taskBounds + ", overlay bounds=" + overlayBounds) + .that(overlayPosition).isEqualTo(params.mPosition); + } + + private enum OverlayPositionTestParams { + LEFT_OVERLAY(CONTAINER_POSITION_LEFT, false /* shouldBeShrunk */), + LEFT_SHRUNK_OVERLAY(CONTAINER_POSITION_LEFT, true /* shouldBeShrunk */), + TOP_OVERLAY(CONTAINER_POSITION_TOP, false /* shouldBeShrunk */), + TOP_SHRUNK_OVERLAY(CONTAINER_POSITION_TOP, true /* shouldBeShrunk */), + RIGHT_OVERLAY(CONTAINER_POSITION_RIGHT, false /* shouldBeShrunk */), + RIGHT_SHRUNK_OVERLAY(CONTAINER_POSITION_RIGHT, true /* shouldBeShrunk */), + BOTTOM_OVERLAY(CONTAINER_POSITION_BOTTOM, false /* shouldBeShrunk */), + BOTTOM_SHRUNK_OVERLAY(CONTAINER_POSITION_BOTTOM, true /* shouldBeShrunk */); + + @SplitPresenter.ContainerPosition + private final int mPosition; + + private final boolean mShouldBeShrunk; + + OverlayPositionTestParams( + @SplitPresenter.ContainerPosition int position, boolean shouldBeShrunk) { + mPosition = position; + mShouldBeShrunk = shouldBeShrunk; + } + + @NonNull + private Rect toOverlayBounds() { + Rect r = new Rect(TASK_BOUNDS); + final int offset = mShouldBeShrunk ? 20 : 0; + switch (mPosition) { + case CONTAINER_POSITION_LEFT: + r.top += offset; + r.right /= 2; + r.bottom -= offset; + break; + case CONTAINER_POSITION_TOP: + r.left += offset; + r.right -= offset; + r.bottom /= 2; + break; + case CONTAINER_POSITION_RIGHT: + r.left = r.right / 2; + r.top += offset; + r.bottom -= offset; + break; + case CONTAINER_POSITION_BOTTOM: + r.left += offset; + r.right -= offset; + r.top = r.bottom / 2; + break; + default: + throw new IllegalArgumentException("Invalid position: " + mPosition); + } + return r; + } + } + /** * A simplified version of {@link SplitController#createOrUpdateOverlayTaskFragmentIfNeeded} */ @@ -892,6 +985,16 @@ public class OverlayPresentationTest { launchOptions, mIntent, activity); } + @Nullable + private TaskFragmentContainer createOrUpdateAlwaysOnTopOverlay( + @NonNull String tag) { + final Bundle launchOptions = new Bundle(); + launchOptions.putBoolean(KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY, false); + launchOptions.putString(KEY_OVERLAY_TAG, tag); + return mSplitController.createOrUpdateOverlayTaskFragmentIfNeeded(mTransaction, + launchOptions, mIntent, createMockActivity()); + } + /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ @NonNull private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) { @@ -901,10 +1004,10 @@ public class OverlayPresentationTest { /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ @NonNull private TaskFragmentContainer createMockTaskFragmentContainer( - @NonNull Activity activity, boolean isVisible) { + @NonNull Activity activity, boolean isOnTop) { final TaskFragmentContainer container = createTfContainer(mSplitController, activity.getTaskId(), activity); - setupTaskFragmentInfo(container, activity, isVisible); + setupTaskFragmentInfo(container, activity, isOnTop); return container; } @@ -916,8 +1019,8 @@ public class OverlayPresentationTest { @NonNull private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag, - boolean isVisible) { - return createTestOverlayContainer(taskId, tag, isVisible, + boolean isOnTop) { + return createTestOverlayContainer(taskId, tag, isOnTop, true /* associateLaunchingActivity */); } @@ -928,11 +1031,9 @@ public class OverlayPresentationTest { null /* launchingActivity */); } - // TODO(b/243518738): add more test coverage on overlay container without activity association - // once we have use cases. @NonNull private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag, - boolean isVisible, boolean associateLaunchingActivity, + boolean isOnTop, boolean associateLaunchingActivity, @Nullable Activity launchingActivity) { final Activity activity = launchingActivity != null ? launchingActivity : createMockActivity(); @@ -943,14 +1044,15 @@ public class OverlayPresentationTest { .setLaunchOptions(Bundle.EMPTY) .setAssociatedActivity(associateLaunchingActivity ? activity : null) .build(); - setupTaskFragmentInfo(overlayContainer, createMockActivity(), isVisible); + setupTaskFragmentInfo(overlayContainer, createMockActivity(), isOnTop); return overlayContainer; } private void setupTaskFragmentInfo(@NonNull TaskFragmentContainer container, @NonNull Activity activity, - boolean isVisible) { - final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity, isVisible); + boolean isOnTop) { + final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity, isOnTop, + isOnTop); container.setInfo(mTransaction, info); mSplitPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), info); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 640b1fced455..05124121fe7b 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -103,8 +103,6 @@ import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; import androidx.window.extensions.layout.WindowLayoutComponentImpl; import androidx.window.extensions.layout.WindowLayoutInfo; -import com.android.window.flags.Flags; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -167,6 +165,7 @@ public class SplitControllerTest { private Consumer<List<SplitInfo>> mEmbeddingCallback; private List<SplitInfo> mSplitInfos; private TransactionManager mTransactionManager; + private ActivityThread mCurrentActivityThread; @Before public void setUp() { @@ -183,10 +182,12 @@ public class SplitControllerTest { }; mSplitController.setSplitInfoCallback(mEmbeddingCallback); mTransactionManager = mSplitController.mTransactionManager; + mCurrentActivityThread = ActivityThread.currentActivityThread(); spyOn(mSplitController); spyOn(mSplitPresenter); spyOn(mEmbeddingCallback); spyOn(mTransactionManager); + spyOn(mCurrentActivityThread); doNothing().when(mSplitPresenter).applyTransaction(any(), anyInt(), anyBoolean()); final Configuration activityConfig = new Configuration(); activityConfig.windowConfiguration.setBounds(TASK_BOUNDS); @@ -200,12 +201,14 @@ public class SplitControllerTest { public void testOnTaskFragmentVanished() { final TaskFragmentContainer tf = createTfContainer(mSplitController, mActivity); doReturn(tf.getTaskFragmentToken()).when(mInfo).getFragmentToken(); + doReturn(createTestTaskContainer()).when(mSplitController).getTaskContainer(TASK_ID); // The TaskFragment has been removed in the server, we only need to cleanup the reference. - mSplitController.onTaskFragmentVanished(mTransaction, mInfo); + mSplitController.onTaskFragmentVanished(mTransaction, mInfo, TASK_ID); verify(mSplitPresenter, never()).deleteTaskFragment(any(), any()); verify(mSplitController).removeContainer(tf); + verify(mSplitController).updateDivider(any(), any(), anyBoolean()); verify(mTransaction, never()).finishActivity(any()); } @@ -1152,7 +1155,7 @@ public class SplitControllerTest { .setTaskFragmentInfo(info)); mSplitController.onTransactionReady(transaction); - verify(mSplitController).onTaskFragmentVanished(any(), eq(info)); + verify(mSplitController).onTaskFragmentVanished(any(), eq(info), anyInt()); verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(), anyInt(), anyBoolean()); } @@ -1161,7 +1164,7 @@ public class SplitControllerTest { public void testOnTransactionReady_taskFragmentParentInfoChanged() { final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo(Configuration.EMPTY, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + DEFAULT_DISPLAY, TASK_ID, true /* visible */, false /* hasDirectActivity */, null /* decorSurface */); transaction.addChange(new TaskFragmentTransaction.Change( TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED) @@ -1555,8 +1558,6 @@ public class SplitControllerTest { @Test public void testIsActivityEmbedded() { - mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG); - assertFalse(mSplitController.isActivityEmbedded(mActivity)); doReturn(true).when(mActivityWindowInfo).isEmbedded(); @@ -1566,8 +1567,6 @@ public class SplitControllerTest { @Test public void testGetEmbeddedActivityWindowInfo() { - mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG); - final boolean isEmbedded = true; final Rect taskBounds = new Rect(0, 0, 1000, 2000); final Rect activityStackBounds = new Rect(0, 0, 500, 2000); @@ -1582,8 +1581,6 @@ public class SplitControllerTest { @Test public void testSetEmbeddedActivityWindowInfoCallback() { - mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG); - final ClientTransactionListenerController controller = ClientTransactionListenerController .getInstance(); spyOn(controller); @@ -1628,7 +1625,7 @@ public class SplitControllerTest { final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID); final Configuration configuration = new Configuration(); final TaskFragmentParentInfo originalInfo = new TaskFragmentParentInfo(configuration, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + DEFAULT_DISPLAY, TASK_ID, true /* visible */, false /* hasDirectActivity */, null /* decorSurface */); mSplitController.onTaskFragmentParentInfoChanged(mock(WindowContainerTransaction.class), TASK_ID, originalInfo); @@ -1637,7 +1634,7 @@ public class SplitControllerTest { // Making a public configuration change while the Task is invisible. configuration.densityDpi += 100; final TaskFragmentParentInfo invisibleInfo = new TaskFragmentParentInfo(configuration, - DEFAULT_DISPLAY, false /* visible */, false /* hasDirectActivity */, + DEFAULT_DISPLAY, TASK_ID, false /* visible */, false /* hasDirectActivity */, null /* decorSurface */); mSplitController.onTaskFragmentParentInfoChanged(mock(WindowContainerTransaction.class), TASK_ID, invisibleInfo); @@ -1649,7 +1646,7 @@ public class SplitControllerTest { // Updates when Task to become visible final TaskFragmentParentInfo visibleInfo = new TaskFragmentParentInfo(configuration, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + DEFAULT_DISPLAY, TASK_ID, true /* visible */, false /* hasDirectActivity */, null /* decorSurface */); mSplitController.onTaskFragmentParentInfoChanged(mock(WindowContainerTransaction.class), TASK_ID, visibleInfo); @@ -1674,7 +1671,8 @@ public class SplitControllerTest { final IBinder activityToken = new Binder(); doReturn(activityToken).when(activity).getActivityToken(); doReturn(activity).when(mSplitController).getActivity(activityToken); - doReturn(activityClientRecord).when(mSplitController).getActivityClientRecord(activity); + doReturn(activityClientRecord).when(mCurrentActivityThread).getActivityClient( + activityToken); doReturn(taskId).when(activity).getTaskId(); doReturn(new ActivityInfo()).when(activity).getActivityInfo(); doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId(); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java index 284723279b80..97f4d0736312 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java @@ -23,6 +23,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer; import static org.junit.Assert.assertEquals; @@ -82,7 +83,7 @@ public class TaskContainerTest { configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + DEFAULT_DISPLAY, TASK_ID, true /* visible */, false /* hasDirectActivity */, null /* decorSurface */)); assertEquals(WINDOWING_MODE_MULTI_WINDOW, @@ -90,7 +91,7 @@ public class TaskContainerTest { configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + DEFAULT_DISPLAY, TASK_ID, true /* visible */, false /* hasDirectActivity */, null /* decorSurface */)); assertEquals(WINDOWING_MODE_FREEFORM, @@ -111,14 +112,14 @@ public class TaskContainerTest { configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + DEFAULT_DISPLAY, TASK_ID, true /* visible */, false /* hasDirectActivity */, null /* decorSurface */)); assertFalse(taskContainer.isInPictureInPicture()); configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_PINNED); taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration, - DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */, + DEFAULT_DISPLAY, TASK_ID, true /* visible */, false /* hasDirectActivity */, null /* decorSurface */)); assertTrue(taskContainer.isInPictureInPicture()); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/layout/WindowLayoutComponentImplTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/layout/WindowLayoutComponentImplTest.java new file mode 100644 index 000000000000..ff0a82fe05d6 --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/layout/WindowLayoutComponentImplTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 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.layout; + +import static org.mockito.Mockito.mock; + +import android.content.Context; +import android.content.ContextWrapper; +import android.platform.test.annotations.Presubmit; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Collections; + +/** + * Test class for {@link WindowLayoutComponentImpl}. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:WindowLayoutComponentImplTest + */ +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class WindowLayoutComponentImplTest { + + private WindowLayoutComponentImpl mWindowLayoutComponent; + + @Before + public void setUp() { + mWindowLayoutComponent = new WindowLayoutComponentImpl( + ApplicationProvider.getApplicationContext(), + mock(DeviceStateManagerFoldingFeatureProducer.class)); + } + + @Test + public void testAddWindowLayoutListenerOnFakeUiContext_noCrash() { + final Context fakeUiContext = createTestContext(); + + mWindowLayoutComponent.addWindowLayoutInfoListener(fakeUiContext, info -> {}); + + mWindowLayoutComponent.onDisplayFeaturesChanged(Collections.emptyList()); + } + + private static Context createTestContext() { + return new FakeUiContext(ApplicationProvider.getApplicationContext()); + } + + /** + * A {@link android.content.Context} overrides {@link android.content.Context#isUiContext} to + * {@code true}. + */ + private static class FakeUiContext extends ContextWrapper { + + FakeUiContext(Context base) { + super(base); + } + + @Override + public boolean isUiContext() { + return true; + } + } +} diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 4b0c7009eaa0..0fc607dd9ecf 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -39,28 +39,6 @@ filegroup { path: "src", } -// Sources that have no dependencies that can be used directly downstream of this library -// TODO(b/322791067): move these sources to WindowManager-Shell-shared -filegroup { - name: "wm_shell_util-sources", - srcs: [ - "src/com/android/wm/shell/animation/Interpolators.java", - "src/com/android/wm/shell/common/bubbles/*.kt", - "src/com/android/wm/shell/common/bubbles/*.java", - "src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt", - "src/com/android/wm/shell/common/split/SplitScreenConstants.java", - "src/com/android/wm/shell/common/TransactionPool.java", - "src/com/android/wm/shell/common/TriangleShape.java", - "src/com/android/wm/shell/common/desktopmode/*.kt", - "src/com/android/wm/shell/draganddrop/DragAndDropConstants.java", - "src/com/android/wm/shell/pip/PipContentOverlay.java", - "src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java", - "src/com/android/wm/shell/sysui/ShellSharedConstants.java", - "src/com/android/wm/shell/util/**/*.java", - ], - path: "src", -} - // Aidls which can be used directly downstream of this library filegroup { name: "wm_shell-aidls", @@ -88,7 +66,7 @@ java_genrule { ], tools: ["protologtool"], cmd: "$(location protologtool) transform-protolog-calls " + - "--protolog-class com.android.internal.protolog.common.ProtoLog " + + "--protolog-class com.android.internal.protolog.ProtoLog " + "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " + "--loggroups-jar $(location :wm_shell_protolog-groups) " + "--viewer-config-file-path /system_ext/etc/wmshell.protolog.pb " + @@ -107,7 +85,7 @@ java_genrule { ], tools: ["protologtool"], cmd: "$(location protologtool) generate-viewer-config " + - "--protolog-class com.android.internal.protolog.common.ProtoLog " + + "--protolog-class com.android.internal.protolog.ProtoLog " + "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " + "--loggroups-jar $(location :wm_shell_protolog-groups) " + "--viewer-config-type json " + @@ -124,7 +102,7 @@ java_genrule { ], tools: ["protologtool"], cmd: "$(location protologtool) generate-viewer-config " + - "--protolog-class com.android.internal.protolog.common.ProtoLog " + + "--protolog-class com.android.internal.protolog.ProtoLog " + "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " + "--loggroups-jar $(location :wm_shell_protolog-groups) " + "--viewer-config-type proto " + @@ -166,6 +144,18 @@ java_library { }, } +java_library { + name: "WindowManager-Shell-lite-proto", + + srcs: [ + "src/com/android/wm/shell/desktopmode/education/data/proto/**/*.proto", + "src/com/android/wm/shell/desktopmode/persistence/*.proto", + ], + proto: { + type: "lite", + }, +} + filegroup { name: "wm_shell-shared-aidls", @@ -185,9 +175,23 @@ java_library { ":wm_shell-shared-aidls", ], static_libs: [ + "androidx.core_core-animation", "androidx.dynamicanimation_dynamicanimation", "jsr330", ], + kotlincflags: ["-Xjvm-default=all"], +} + +java_library { + name: "WindowManager-Shell-shared-desktopMode", + + srcs: [ + "shared/**/desktopmode/*.java", + "shared/**/desktopmode/*.kt", + ], + static_libs: [ + "com.android.window.flags.window-aconfig-java", + ], } android_library { @@ -197,15 +201,16 @@ android_library { // TODO(b/168581922) protologtool do not support kotlin(*.kt) ":wm_shell-sources-kt", ":wm_shell-aidls", + ":wm_shell-shared-aidls", ], resource_dirs: [ "res", ], static_libs: [ "androidx.appcompat_appcompat", - "androidx.core_core-animation", "androidx.core_core-ktx", "androidx.arch.core_core-runtime", + "androidx.datastore_datastore", "androidx.compose.material3_material3", "androidx-constraintlayout_constraintlayout", "androidx.dynamicanimation_dynamicanimation", @@ -215,8 +220,8 @@ android_library { "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", "//frameworks/libs/systemui:iconloader_base", "com_android_wm_shell_flags_lib", - "com.android.window.flags.window-aconfig-java", "WindowManager-Shell-proto", + "WindowManager-Shell-lite-proto", "WindowManager-Shell-shared", "perfetto_trace_java_protos", "dagger2", @@ -227,6 +232,13 @@ android_library { // *.kt sources are inside a filegroup. "kotlin-annotations", ], + required: [ + "wmshell.protolog.json.gz", + "wmshell.protolog.pb", + ], + flags_packages: [ + "com_android_wm_shell_flags", + ], kotlincflags: ["-Xjvm-default=all"], manifest: "AndroidManifest.xml", plugins: ["dagger2-compiler"], diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml index bcb1d292fce2..3b739c3d5817 100644 --- a/libs/WindowManager/Shell/AndroidManifest.xml +++ b/libs/WindowManager/Shell/AndroidManifest.xml @@ -23,16 +23,19 @@ <uses-permission android:name="android.permission.ROTATE_SURFACE_FLINGER" /> <uses-permission android:name="android.permission.WAKEUP_SURFACE_FLINGER" /> <uses-permission android:name="android.permission.READ_FRAME_BUFFER" /> + <uses-permission android:name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" /> <application> <activity android:name=".desktopmode.DesktopWallpaperActivity" android:excludeFromRecents="true" android:launchMode="singleInstance" + android:showForAllUsers="true" android:theme="@style/DesktopWallpaperTheme" /> <activity android:name=".bubbles.shortcut.CreateBubbleShortcutActivity" + android:featureFlag="com.android.wm.shell.enable_retrievable_bubbles" android:exported="true" android:excludeFromRecents="true" android:theme="@android:style/Theme.NoDisplay" @@ -46,6 +49,7 @@ <activity android:name=".bubbles.shortcut.ShowBubblesActivity" + android:featureFlag="com.android.wm.shell.enable_retrievable_bubbles" android:exported="true" android:excludeFromRecents="true" android:theme="@android:style/Theme.NoDisplay" > diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 112eb617e7a6..526ccd55ce3d 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -25,11 +25,10 @@ flag { } flag { - name: "enable_pip2_implementation" + name: "enable_pip2" namespace: "multitasking" description: "Enables the new implementation of PiP (PiP2)" - bug: "290220798" - is_fixed_read_only: true + bug: "311462191" } flag { @@ -113,11 +112,42 @@ flag { } flag { - name: "animate_bubble_size_change" + name: "enable_taskbar_on_phones" namespace: "multitasking" - description: "Turns on the animation for bubble bar icons size change" - bug: "335575529" + description: "Enables taskbar on phones" + bug: "348007377" metadata { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enable_bubble_bar_in_persistent_task_bar" + namespace: "multitasking" + description: "Enable bubble bar to be shown in the persistent task bar" + bug: "346391377" +} + +flag { + name: "bubble_view_info_executors" + namespace: "multitasking" + description: "Use executors to inflate bubble views" + bug: "353894869" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_bubble_to_fullscreen" + namespace: "multitasking" + description: "Enable an option to move bubbles to fullscreen" + bug: "363326492" +} + +flag { + name: "enable_flexible_split" + namespace: "multitasking" + description: "Enables flexibile split feature for split screen" + bug: "349828130" +} diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp new file mode 100644 index 000000000000..b6db6d93499d --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp @@ -0,0 +1,95 @@ +// Copyright (C) 2024 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], + default_team: "trendy_team_multitasking_windowing", +} + +android_app { + name: "WMShellRobolectricScreenshotTestApp", + platform_apis: true, + certificate: "platform", + static_libs: [ + "WindowManager-Shell", + "platform-screenshot-diff-core", + ], + asset_dirs: ["goldens/robolectric"], + manifest: "AndroidManifestRobolectric.xml", + use_resource_processor: true, +} + +android_robolectric_test { + name: "WMShellRobolectricScreenshotTests", + instrumentation_for: "WMShellRobolectricScreenshotTestApp", + upstream: true, + java_resource_dirs: [ + "robolectric/config", + ], + srcs: [ + "src/**/*.kt", + ], + static_libs: [ + "junit", + "androidx.test.runner", + "androidx.test.rules", + "androidx.test.ext.junit", + "truth", + "platform-parametric-runner-lib", + ], + auto_gen_config: true, +} + +android_test { + name: "WMShellMultivalentScreenshotTestsOnDevice", + srcs: [ + "src/**/*.kt", + ], + static_libs: [ + "WindowManager-Shell", + "junit", + "androidx.test.runner", + "androidx.test.rules", + "androidx.test.ext.junit", + "truth", + "platform-parametric-runner-lib", + "platform-screenshot-diff-core", + ], + libs: [ + "android.test.base.stubs.system", + "android.test.runner.stubs.system", + ], + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + kotlincflags: ["-Xjvm-default=all"], + optimize: { + enabled: false, + }, + test_suites: ["device-tests"], + platform_apis: true, + certificate: "platform", + aaptflags: [ + "--extra-packages", + "com.android.wm.shell", + ], + manifest: "AndroidManifest.xml", + asset_dirs: ["goldens/onDevice"], +} diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifest.xml b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifest.xml new file mode 100644 index 000000000000..467dc6a5cb81 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifest.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.wm.shell.multivalentscreenshot"> + + <application android:debuggable="true" android:supportsRtl="true" > + <uses-library android:name="android.test.runner" /> + <activity + android:name="platform.test.screenshot.ScreenshotActivity" + android:exported="true"> + </activity> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:label="Multivalent screenshot tests for WindowManager-Shell" + android:targetPackage="com.android.wm.shell.multivalentscreenshot"> + </instrumentation> + + <!-- this permission is required by Tuner Service in screenshot tests --> + <uses-permission android:name="android.permission.MANAGE_USERS" /> +</manifest> diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml new file mode 100644 index 000000000000..b4bdaeaf0eac --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.wm.shell.multivalentscreenshot"> + <application android:debuggable="true" android:supportsRtl="true"> + <activity + android:name="platform.test.screenshot.ScreenshotActivity" + android:exported="true"> + </activity> + </application> +</manifest> diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidTest.xml b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidTest.xml new file mode 100644 index 000000000000..75793ae69d27 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidTest.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> +<configuration description="Runs Tests for WindowManagerShellLib"> + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" /> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="install-arg" value="-t" /> + <option name="test-file-name" value="WMShellMultivalentScreenshotTestsOnDevice.apk" /> + </target_preparer> + + <option name="test-suite-tag" value="apct" /> + <option name="test-tag" value="WMShellMultivalentScreenshotTestsOnDevice" /> + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="directory-keys" value="/data/user/0/com.android.wm.shell.multivalentscreenshot/files/wmshell_screenshots" /> + <option name="collect-on-run-ended-only" value="true" /> + </metrics_collector> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.wm.shell.multivalentscreenshot" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/OWNERS b/libs/WindowManager/Shell/multivalentScreenshotTests/OWNERS new file mode 100644 index 000000000000..dc11241fb76b --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/OWNERS @@ -0,0 +1,4 @@ +atsjenk@google.com +liranb@google.com +madym@google.com +mpodolian@google.com
\ No newline at end of file diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png Binary files differnew file mode 100644 index 000000000000..991cdcf09416 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png Binary files differnew file mode 100644 index 000000000000..991cdcf09416 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png Binary files differnew file mode 100644 index 000000000000..c72944222e66 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png Binary files differnew file mode 100644 index 000000000000..c72944222e66 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/robolectric/config/robolectric.properties b/libs/WindowManager/Shell/multivalentScreenshotTests/robolectric/config/robolectric.properties new file mode 100644 index 000000000000..d50d976c9e84 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/robolectric/config/robolectric.properties @@ -0,0 +1,3 @@ +sdk=NEWEST_SDK +graphicsMode=NATIVE + diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt new file mode 100644 index 000000000000..f09969d253d3 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 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.bubbles + +import android.view.LayoutInflater +import com.android.wm.shell.shared.bubbles.BubblePopupView +import com.android.wm.shell.testing.goldenpathmanager.WMShellGoldenPathManager +import com.android.wm.shell.R +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters +import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.Displays +import platform.test.screenshot.ViewScreenshotTestRule +import platform.test.screenshot.getEmulatedDevicePathConfig + +@RunWith(ParameterizedAndroidJunit4::class) +class BubbleEducationViewScreenshotTest(emulationSpec: DeviceEmulationSpec) { + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun getTestSpecs() = DeviceEmulationSpec.forDisplays(Displays.Phone, isLandscape = false) + } + + @get:Rule + val screenshotRule = + ViewScreenshotTestRule( + emulationSpec, + WMShellGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)) + ) + + @Test + fun bubblesEducation() { + screenshotRule.screenshotTest("bubbles_education") { activity -> + activity.actionBar?.hide() + val view = + LayoutInflater.from(activity) + .inflate(R.layout.bubble_bar_stack_education, null) as BubblePopupView + view.setup() + view + } + } +} diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/testing/goldenpathmanager/WMShellGoldenPathManager.kt b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/testing/goldenpathmanager/WMShellGoldenPathManager.kt new file mode 100644 index 000000000000..901b79b9b1a0 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/testing/goldenpathmanager/WMShellGoldenPathManager.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 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.testing.goldenpathmanager + +import android.os.Build +import androidx.test.platform.app.InstrumentationRegistry +import platform.test.screenshot.GoldenPathManager +import platform.test.screenshot.PathConfig + +/** A WM Shell specific implementation of [GoldenPathManager]. */ +class WMShellGoldenPathManager(pathConfig: PathConfig) : + GoldenPathManager( + appContext = InstrumentationRegistry.getInstrumentation().context, + assetsPathRelativeToBuildRoot = assetPath, + deviceLocalPath = deviceLocalPath, + pathConfig = pathConfig, + ) { + + private companion object { + private const val ASSETS_PATH = + "frameworks/base/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice" + private const val ASSETS_PATH_ROBO = + "frameworks/base/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/" + + "robolectric" + private val assetPath: String + get() = if (Build.FINGERPRINT.contains("robolectric")) ASSETS_PATH_ROBO else ASSETS_PATH + private val deviceLocalPath: String + get() = + InstrumentationRegistry.getInstrumentation() + .targetContext + .filesDir + .absolutePath + .toString() + "/wmshell_screenshots" + } + override fun toString(): String { + // This string is appended to all actual/expected screenshots on the device, so make sure + // it is a static value. + return "WMShellGoldenPathManager" + } +} diff --git a/libs/WindowManager/Shell/multivalentScreenshotTestsForDevice b/libs/WindowManager/Shell/multivalentScreenshotTestsForDevice new file mode 120000 index 000000000000..e879efc81ec1 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTestsForDevice @@ -0,0 +1 @@ +multivalentScreenshotTests
\ No newline at end of file diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt index 9e1440d5716b..b38d00da6dfa 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt @@ -27,10 +27,10 @@ import android.view.WindowManager import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.internal.protolog.common.ProtoLog +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.R import com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT -import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.android.wm.shell.shared.bubbles.BubbleBarLocation import com.google.common.truth.Truth.assertThat import com.google.common.util.concurrent.MoreExecutors.directExecutor import org.junit.Before @@ -268,7 +268,8 @@ class BubblePositionerTest { ) positioner.update(deviceConfig) val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) - val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + val bubble = + Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor(), directExecutor()) assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(MAX_HEIGHT) } @@ -294,6 +295,7 @@ class BubblePositionerTest { 0 /* taskId */, null /* locus */, true /* isDismissable */, + directExecutor(), directExecutor() ) {} @@ -322,6 +324,7 @@ class BubblePositionerTest { 0 /* taskId */, null /* locus */, true /* isDismissable */, + directExecutor(), directExecutor() ) {} @@ -416,7 +419,8 @@ class BubblePositionerTest { positioner.update(deviceConfig) val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) - val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + val bubble = + Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor(), directExecutor()) // This bubble will have max height so it'll always be top aligned assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) @@ -433,7 +437,8 @@ class BubblePositionerTest { positioner.update(deviceConfig) val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) - val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + val bubble = + Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor(), directExecutor()) // Always top aligned in phone portrait assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) @@ -452,7 +457,8 @@ class BubblePositionerTest { positioner.update(deviceConfig) val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) - val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + val bubble = + Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor(), directExecutor()) // This bubble will have max height which is always top aligned on small tablets assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) @@ -470,7 +476,8 @@ class BubblePositionerTest { positioner.update(deviceConfig) val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) - val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + val bubble = + Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor(), directExecutor()) // This bubble will have max height which is always top aligned on small tablets assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) @@ -489,7 +496,8 @@ class BubblePositionerTest { positioner.update(deviceConfig) val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) - val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + val bubble = + Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor(), directExecutor()) // This bubble will have max height which is always top aligned on landscape, large tablet assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */)) @@ -507,7 +515,8 @@ class BubblePositionerTest { positioner.update(deviceConfig) val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) - val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor()) + val bubble = + Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor(), directExecutor()) val manageButtonHeight = context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height) diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt index 327e2059557c..96ffa03a1f65 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt @@ -32,7 +32,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry import com.android.internal.logging.testing.UiEventLoggerFake -import com.android.internal.protolog.common.ProtoLog +import com.android.internal.protolog.ProtoLog import com.android.launcher3.icons.BubbleIconFactory import com.android.wm.shell.Flags import com.android.wm.shell.R @@ -53,6 +53,7 @@ import org.junit.runner.RunWith import org.mockito.kotlin.mock import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import com.android.wm.shell.shared.bubbles.BubbleBarLocation import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit import java.util.function.Consumer @@ -101,6 +102,7 @@ class BubbleStackViewTest { BubbleLogger(UiEventLoggerFake()), positioner, BubbleEducationController(context), + shellExecutor, shellExecutor ) bubbleStackViewManager = FakeBubbleStackViewManager() @@ -363,6 +365,7 @@ class BubbleStackViewTest { /* taskId= */ 0, "locus", /* isDismissable= */ true, + directExecutor(), directExecutor() ) {} inflateBubble(bubble) @@ -372,7 +375,8 @@ class BubbleStackViewTest { private fun createAndInflateBubble(): Bubble { val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button) - val bubble = Bubble.createAppBubble(intent, UserHandle(1), icon, directExecutor()) + val bubble = + Bubble.createAppBubble(intent, UserHandle(1), icon, directExecutor(), directExecutor()) inflateBubble(bubble) return bubble } @@ -458,5 +462,7 @@ class BubbleStackViewTest { override fun isShowingAsBubbleBar(): Boolean = false override fun hideCurrentInputMethod() {} + + override fun updateBubbleBarLocation(location: BubbleBarLocation) {} } } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt new file mode 100644 index 000000000000..9fdde128ce41 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt @@ -0,0 +1,349 @@ +/* + * Copyright (C) 2024 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.bubbles + +import android.content.Context +import android.content.pm.LauncherApps +import android.content.pm.ShortcutInfo +import android.content.res.Resources +import android.graphics.Color +import android.os.Handler +import android.os.UserManager +import android.view.IWindowManager +import android.view.WindowManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.internal.protolog.ProtoLog +import com.android.internal.statusbar.IStatusBarService +import com.android.launcher3.icons.BubbleIconFactory +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.WindowManagerShellWrapper +import com.android.wm.shell.bubbles.properties.BubbleProperties +import com.android.wm.shell.bubbles.storage.BubblePersistentRepository +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayInsetsController +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.TaskStackListenerImpl +import com.android.wm.shell.shared.TransactionPool +import com.android.wm.shell.sysui.ShellCommandHandler +import com.android.wm.shell.sysui.ShellController +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.taskview.TaskView +import com.android.wm.shell.taskview.TaskViewTransitions +import com.android.wm.shell.transition.Transitions +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors.directExecutor +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +/** Test inflating bubbles with [BubbleViewInfoTask]. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleViewInfoTaskTest { + + private val context = ApplicationProvider.getApplicationContext<Context>() + private lateinit var metadataFlagListener: Bubbles.BubbleMetadataFlagListener + private lateinit var iconFactory: BubbleIconFactory + private lateinit var bubbleController: BubbleController + private lateinit var mainExecutor: TestExecutor + private lateinit var bgExecutor: TestExecutor + private lateinit var bubbleStackView: BubbleStackView + private lateinit var bubblePositioner: BubblePositioner + private lateinit var expandedViewManager: BubbleExpandedViewManager + + private val bubbleTaskViewFactory = BubbleTaskViewFactory { + BubbleTaskView(mock<TaskView>(), directExecutor()) + } + + @Before + fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false + metadataFlagListener = Bubbles.BubbleMetadataFlagListener {} + iconFactory = + BubbleIconFactory( + context, + 60, + 30, + Color.RED, + context.resources.getDimensionPixelSize(R.dimen.importance_ring_stroke_width) + ) + + mainExecutor = TestExecutor() + bgExecutor = TestExecutor() + val windowManager = context.getSystemService(WindowManager::class.java) + val shellInit = ShellInit(mainExecutor) + val shellCommandHandler = ShellCommandHandler() + val shellController = + ShellController( + context, + shellInit, + shellCommandHandler, + mock<DisplayInsetsController>(), + mainExecutor + ) + bubblePositioner = BubblePositioner(context, windowManager) + val bubbleData = + BubbleData( + context, + mock<BubbleLogger>(), + bubblePositioner, + BubbleEducationController(context), + mainExecutor, + bgExecutor + ) + + val surfaceSynchronizer = { obj: Runnable -> obj.run() } + + val bubbleDataRepository = + BubbleDataRepository( + mock<LauncherApps>(), + mainExecutor, + bgExecutor, + BubblePersistentRepository(context) + ) + + bubbleController = + BubbleController( + context, + shellInit, + shellCommandHandler, + shellController, + bubbleData, + surfaceSynchronizer, + FloatingContentCoordinator(), + bubbleDataRepository, + mock<IStatusBarService>(), + windowManager, + WindowManagerShellWrapper(mainExecutor), + mock<UserManager>(), + mock<LauncherApps>(), + mock<BubbleLogger>(), + mock<TaskStackListenerImpl>(), + mock<ShellTaskOrganizer>(), + bubblePositioner, + mock<DisplayController>(), + null, + null, + mainExecutor, + mock<Handler>(), + bgExecutor, + mock<TaskViewTransitions>(), + mock<Transitions>(), + SyncTransactionQueue(TransactionPool(), mainExecutor), + mock<IWindowManager>(), + mock<BubbleProperties>() + ) + + val bubbleStackViewManager = BubbleStackViewManager.fromBubbleController(bubbleController) + bubbleStackView = + BubbleStackView( + context, + bubbleStackViewManager, + bubblePositioner, + bubbleData, + surfaceSynchronizer, + FloatingContentCoordinator(), + bubbleController, + mainExecutor + ) + expandedViewManager = BubbleExpandedViewManager.fromBubbleController(bubbleController) + } + + @Test + fun start_runsOnExecutors() { + val bubble = createBubbleWithShortcut() + val task = createBubbleViewInfoTask(bubble) + + task.start() + + assertThat(bubble.isInflated).isFalse() + assertThat(bubble.expandedView).isNull() + assertThat(task.isFinished).isFalse() + + bgExecutor.flushAll() + assertThat(bubble.isInflated).isFalse() + assertThat(bubble.expandedView).isNull() + assertThat(task.isFinished).isFalse() + + mainExecutor.flushAll() + assertThat(bubble.isInflated).isTrue() + assertThat(bubble.expandedView).isNotNull() + assertThat(task.isFinished).isTrue() + } + + @Test + fun startSync_runsImmediately() { + val bubble = createBubbleWithShortcut() + val task = createBubbleViewInfoTask(bubble) + + task.startSync() + assertThat(bubble.isInflated).isTrue() + assertThat(bubble.expandedView).isNotNull() + assertThat(task.isFinished).isTrue() + } + + @Test + fun start_calledTwice_throwsIllegalStateException() { + val bubble = createBubbleWithShortcut() + val task = createBubbleViewInfoTask(bubble) + task.start() + Assert.assertThrows(IllegalStateException::class.java) { task.start() } + } + + @Test + fun startSync_calledTwice_throwsIllegalStateException() { + val bubble = createBubbleWithShortcut() + val task = createBubbleViewInfoTask(bubble) + task.startSync() + Assert.assertThrows(IllegalStateException::class.java) { task.startSync() } + } + + @Test + fun start_callbackNotified() { + val bubble = createBubbleWithShortcut() + var bubbleFromCallback: Bubble? = null + val callback = BubbleViewInfoTask.Callback { b: Bubble? -> bubbleFromCallback = b } + val task = createBubbleViewInfoTask(bubble, callback) + task.start() + bgExecutor.flushAll() + mainExecutor.flushAll() + assertThat(bubbleFromCallback).isSameInstanceAs(bubble) + } + + @Test + fun startSync_callbackNotified() { + val bubble = createBubbleWithShortcut() + var bubbleFromCallback: Bubble? = null + val callback = BubbleViewInfoTask.Callback { b: Bubble? -> bubbleFromCallback = b } + val task = createBubbleViewInfoTask(bubble, callback) + task.startSync() + assertThat(bubbleFromCallback).isSameInstanceAs(bubble) + } + + @Test + fun cancel_beforeBackgroundWorkStarts_bubbleNotInflated() { + val bubble = createBubbleWithShortcut() + val task = createBubbleViewInfoTask(bubble) + task.start() + + // Cancel before allowing background or main executor to run + task.cancel() + bgExecutor.flushAll() + mainExecutor.flushAll() + + assertThat(bubble.isInflated).isFalse() + assertThat(bubble.expandedView).isNull() + assertThat(task.isFinished).isTrue() + } + + @Test + fun cancel_afterBackgroundWorkBeforeMainThreadWork_bubbleNotInflated() { + val bubble = createBubbleWithShortcut() + val task = createBubbleViewInfoTask(bubble) + task.start() + + // Cancel after background executor runs, but before main executor runs + bgExecutor.flushAll() + task.cancel() + mainExecutor.flushAll() + + assertThat(bubble.isInflated).isFalse() + assertThat(bubble.expandedView).isNull() + assertThat(task.isFinished).isTrue() + } + + @Test + fun cancel_beforeStart_bubbleNotInflated() { + val bubble = createBubbleWithShortcut() + val task = createBubbleViewInfoTask(bubble) + task.cancel() + task.start() + bgExecutor.flushAll() + mainExecutor.flushAll() + + assertThat(task.isFinished).isTrue() + assertThat(bubble.isInflated).isFalse() + assertThat(bubble.expandedView).isNull() + } + + private fun createBubbleWithShortcut(): Bubble { + val shortcutInfo = ShortcutInfo.Builder(context, "mockShortcutId").build() + return Bubble( + "mockKey", + shortcutInfo, + 1000, + Resources.ID_NULL, + "mockTitle", + 0 /* taskId */, + "mockLocus", + true /* isDismissible */, + mainExecutor, + bgExecutor, + metadataFlagListener + ) + } + + private fun createBubbleViewInfoTask( + bubble: Bubble, + callback: BubbleViewInfoTask.Callback? = null + ): BubbleViewInfoTask { + return BubbleViewInfoTask( + bubble, + context, + expandedViewManager, + bubbleTaskViewFactory, + bubblePositioner, + bubbleStackView, + null /* layerView */, + iconFactory, + false /* skipInflation */, + callback, + mainExecutor, + bgExecutor + ) + } + + private class TestExecutor : ShellExecutor { + + private val runnables: MutableList<Runnable> = mutableListOf() + + override fun execute(runnable: Runnable) { + runnables.add(runnable) + } + + override fun executeDelayed(runnable: Runnable, delayMillis: Long) { + execute(runnable) + } + + override fun removeCallbacks(runnable: Runnable?) {} + + override fun hasCallback(runnable: Runnable?): Boolean = false + + fun flushAll() { + while (runnables.isNotEmpty()) { + runnables.removeAt(0).run() + } + } + } +} diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt new file mode 100644 index 000000000000..35d459f27534 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2024 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.bubbles.bar + +import android.app.ActivityManager +import android.content.Context +import android.graphics.Insets +import android.graphics.Rect +import android.view.LayoutInflater +import android.view.View +import android.view.WindowManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.Bubble +import com.android.wm.shell.bubbles.BubbleData +import com.android.wm.shell.bubbles.BubbleExpandedViewManager +import com.android.wm.shell.bubbles.BubblePositioner +import com.android.wm.shell.bubbles.BubbleTaskView +import com.android.wm.shell.bubbles.BubbleTaskViewFactory +import com.android.wm.shell.bubbles.DeviceConfig +import com.android.wm.shell.bubbles.RegionSamplingProvider +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import com.android.wm.shell.shared.handles.RegionSamplingHelper +import com.android.wm.shell.taskview.TaskView +import com.android.wm.shell.taskview.TaskViewTaskController +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.util.Collections +import java.util.concurrent.Executor + +/** Tests for [BubbleBarExpandedViewTest] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleBarExpandedViewTest { + companion object { + const val SCREEN_WIDTH = 2000 + const val SCREEN_HEIGHT = 1000 + } + + private val context = ApplicationProvider.getApplicationContext<Context>() + private val windowManager = context.getSystemService(WindowManager::class.java) + + private lateinit var mainExecutor: TestExecutor + private lateinit var bgExecutor: TestExecutor + + private lateinit var expandedViewManager: BubbleExpandedViewManager + private lateinit var positioner: BubblePositioner + private lateinit var bubbleTaskView: BubbleTaskView + + private lateinit var bubbleExpandedView: BubbleBarExpandedView + private var testableRegionSamplingHelper: TestableRegionSamplingHelper? = null + private var regionSamplingProvider: TestRegionSamplingProvider? = null + + @Before + fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false + mainExecutor = TestExecutor() + bgExecutor = TestExecutor() + positioner = BubblePositioner(context, windowManager) + positioner.setShowingInBubbleBar(true) + val deviceConfig = + DeviceConfig( + windowBounds = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT), + isLargeScreen = true, + isSmallTablet = false, + isLandscape = true, + isRtl = false, + insets = Insets.of(10, 20, 30, 40) + ) + positioner.update(deviceConfig) + + expandedViewManager = createExpandedViewManager() + bubbleTaskView = FakeBubbleTaskViewFactory().create() + + val inflater = LayoutInflater.from(context) + + regionSamplingProvider = TestRegionSamplingProvider() + + bubbleExpandedView = (inflater.inflate( + R.layout.bubble_bar_expanded_view, null, false /* attachToRoot */ + ) as BubbleBarExpandedView) + bubbleExpandedView.initialize( + expandedViewManager, + positioner, + false /* isOverflow */, + bubbleTaskView, + mainExecutor, + bgExecutor, + regionSamplingProvider + ) + + getInstrumentation().runOnMainSync(Runnable { + bubbleExpandedView.onAttachedToWindow() + // Helper should be created once attached to window + testableRegionSamplingHelper = regionSamplingProvider!!.helper + }) + } + + @After + fun tearDown() { + testableRegionSamplingHelper?.stopAndDestroy() + } + + @Test + fun testCreateSamplingHelper_onAttach() { + assertThat(testableRegionSamplingHelper).isNotNull() + } + + @Test + fun testDestroySamplingHelper_onDetach() { + bubbleExpandedView.onDetachedFromWindow() + assertThat(testableRegionSamplingHelper!!.isDestroyed).isTrue() + } + + @Test + fun testStopSampling_onDragStart() { + bubbleExpandedView.setContentVisibility(true) + assertThat(testableRegionSamplingHelper!!.isStarted).isTrue() + + bubbleExpandedView.setDragging(true) + assertThat(testableRegionSamplingHelper!!.isStopped).isTrue() + } + + @Test + fun testStartSampling_onDragEnd() { + bubbleExpandedView.setDragging(true) + bubbleExpandedView.setContentVisibility(true) + assertThat(testableRegionSamplingHelper!!.isStopped).isTrue() + + bubbleExpandedView.setDragging(false) + assertThat(testableRegionSamplingHelper!!.isStarted).isTrue() + } + + @Test + fun testStartSampling_onContentVisible() { + bubbleExpandedView.setContentVisibility(true) + assertThat(testableRegionSamplingHelper!!.setWindowVisible).isTrue() + assertThat(testableRegionSamplingHelper!!.isStarted).isTrue() + } + + @Test + fun testStopSampling_onContentInvisible() { + bubbleExpandedView.setContentVisibility(false) + + assertThat(testableRegionSamplingHelper!!.setWindowInvisible).isTrue() + assertThat(testableRegionSamplingHelper!!.isStopped).isTrue() + } + + @Test + fun testSampling_startStopAnimating_visible() { + bubbleExpandedView.isAnimating = true + bubbleExpandedView.setContentVisibility(true) + assertThat(testableRegionSamplingHelper!!.isStopped).isTrue() + + bubbleExpandedView.isAnimating = false + assertThat(testableRegionSamplingHelper!!.isStarted).isTrue() + } + + @Test + fun testSampling_startStopAnimating_invisible() { + bubbleExpandedView.isAnimating = true + bubbleExpandedView.setContentVisibility(false) + assertThat(testableRegionSamplingHelper!!.isStopped).isTrue() + testableRegionSamplingHelper!!.reset() + + bubbleExpandedView.isAnimating = false + assertThat(testableRegionSamplingHelper!!.isStopped).isTrue() + } + + private inner class FakeBubbleTaskViewFactory : BubbleTaskViewFactory { + override fun create(): BubbleTaskView { + val taskViewTaskController = mock<TaskViewTaskController>() + val taskView = TaskView(context, taskViewTaskController) + val taskInfo = mock<ActivityManager.RunningTaskInfo>() + whenever(taskViewTaskController.taskInfo).thenReturn(taskInfo) + return BubbleTaskView(taskView, mainExecutor) + } + } + + private inner class TestRegionSamplingProvider : RegionSamplingProvider { + + lateinit var helper: TestableRegionSamplingHelper + + override fun createHelper( + sampledView: View?, + callback: RegionSamplingHelper.SamplingCallback?, + backgroundExecutor: Executor?, + mainExecutor: Executor? + ): RegionSamplingHelper { + helper = TestableRegionSamplingHelper(sampledView, callback, backgroundExecutor, + mainExecutor) + return helper + } + } + + private inner class TestableRegionSamplingHelper( + sampledView: View?, + samplingCallback: SamplingCallback?, + backgroundExecutor: Executor?, + mainExecutor: Executor? + ) : RegionSamplingHelper(sampledView, samplingCallback, backgroundExecutor, mainExecutor) { + + var isStarted = false + var isStopped = false + var isDestroyed = false + var setWindowVisible = false + var setWindowInvisible = false + + override fun start(initialSamplingBounds: Rect) { + super.start(initialSamplingBounds) + isStarted = true + } + + override fun stop() { + super.stop() + isStopped = true + } + + override fun stopAndDestroy() { + super.stopAndDestroy() + isDestroyed = true + } + + override fun setWindowVisible(visible: Boolean) { + super.setWindowVisible(visible) + if (visible) { + setWindowVisible = true + } else { + setWindowInvisible = true + } + } + + fun reset() { + isStarted = false + isStopped = false + isDestroyed = false + setWindowVisible = false + setWindowInvisible = false + } + } + + private fun createExpandedViewManager(): BubbleExpandedViewManager { + return object : BubbleExpandedViewManager { + override val overflowBubbles: List<Bubble> + get() = Collections.emptyList() + + override fun setOverflowListener(listener: BubbleData.Listener) { + } + + override fun collapseStack() { + } + + override fun updateWindowFlagsForBackpress(intercept: Boolean) { + } + + override fun promoteBubbleFromOverflow(bubble: Bubble) { + } + + override fun removeBubble(key: String, reason: Int) { + } + + override fun dismissBubble(bubble: Bubble, reason: Int) { + } + + override fun setAppBubbleTaskId(key: String, taskId: Int) { + } + + override fun isStackExpanded(): Boolean { + return true + } + + override fun isShowingAsBubbleBar(): Boolean { + return true + } + + override fun hideCurrentInputMethod() { + } + + override fun updateBubbleBarLocation(location: BubbleBarLocation) { + } + } + } + + private class TestExecutor : ShellExecutor { + + private val runnables: MutableList<Runnable> = mutableListOf() + + override fun execute(runnable: Runnable) { + runnables.add(runnable) + } + + override fun executeDelayed(runnable: Runnable, delayMillis: Long) { + execute(runnable) + } + + override fun removeCallbacks(runnable: Runnable?) {} + + override fun hasCallback(runnable: Runnable?): Boolean = false + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt index ace2c131050c..ecb2b25a02f1 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt @@ -27,16 +27,16 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation -import com.android.internal.protolog.common.ProtoLog +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.R import com.android.wm.shell.bubbles.BubblePositioner import com.android.wm.shell.bubbles.DeviceConfig -import com.android.wm.shell.common.bubbles.BaseBubblePinController -import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_IN_DURATION -import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_OUT_DURATION -import com.android.wm.shell.common.bubbles.BubbleBarLocation -import com.android.wm.shell.common.bubbles.BubbleBarLocation.LEFT -import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT +import com.android.wm.shell.shared.bubbles.BaseBubblePinController +import com.android.wm.shell.shared.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_IN_DURATION +import com.android.wm.shell.shared.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_OUT_DURATION +import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import com.android.wm.shell.shared.bubbles.BubbleBarLocation.LEFT +import com.android.wm.shell.shared.bubbles.BubbleBarLocation.RIGHT import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before diff --git a/libs/WindowManager/Shell/res/drawable/decor_maximize_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_maximize_button_dark.xml index ab4e29ac97e5..7b3353462bd6 100644 --- a/libs/WindowManager/Shell/res/drawable/decor_maximize_button_dark.xml +++ b/libs/WindowManager/Shell/res/drawable/decor_maximize_button_dark.xml @@ -17,19 +17,16 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="32.0dp" android:height="32.0dp" - android:viewportWidth="32.0" - android:viewportHeight="32.0" - android:tint="@color/decor_button_dark_color"> + android:viewportWidth="24.0" + android:viewportHeight="24.0"> <group android:scaleX="0.5" android:scaleY="0.5" - android:translateX="8.0" - android:translateY="8.0" > + android:translateX="6.0" + android:translateY="6.0" > <path - android:fillColor="@android:color/white" - android:pathData="M2.0,4.0l0.0,16.0l28.0,0.0L30.0,4.0L2.0,4.0zM26.0,16.0L6.0,16.0L6.0,8.0l20.0,0.0L26.0,16.0z"/> - <path - android:fillColor="@android:color/white" - android:pathData="M2.0,24.0l28.0,0.0l0.0,4.0l-28.0,0.0z"/> + android:fillColor="@android:color/black" + android:fillType="evenOdd" + android:pathData="M23.0,1.0v22.0H1V1h22zm-3,19H4V4h16v16z"/> </group> </vector> diff --git a/libs/WindowManager/Shell/res/drawable/decor_restore_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_restore_button_dark.xml new file mode 100644 index 000000000000..91c8f544c08d --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/decor_restore_button_dark.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 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="32.0dp" + android:height="32.0dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <group android:scaleX="0.5" + android:scaleY="0.5" + android:translateX="6.0" + android:translateY="6.0" > + <path + android:fillColor="@android:color/black" + android:fillType="evenOdd" + android:pathData="M23,16H8V1h15v15zm-12,-3V4h9v9h-9zM4,8H1v15h15v-3H4V8z"/> + </group> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_header_ic_minimize.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_header_ic_minimize.xml new file mode 100644 index 000000000000..b35dc022e210 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_header_ic_minimize.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector + xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <path + android:fillColor="#FF000000" + android:pathData="M6,21V19H18V21Z"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_manage_windows.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_manage_windows.xml new file mode 100644 index 000000000000..7d912a24c443 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_manage_windows.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" android:tint="?attr/colorControlNormal"> + <path android:fillColor="@android:color/black" android:pathData="M160,880Q127,880 103.5,856.5Q80,833 80,800L80,440Q80,407 103.5,383.5Q127,360 160,360L240,360L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,520Q880,553 856.5,576.5Q833,600 800,600L720,600L720,800Q720,833 696.5,856.5Q673,880 640,880L160,880ZM160,800L640,800Q640,800 640,800Q640,800 640,800L640,520L160,520L160,800Q160,800 160,800Q160,800 160,800ZM720,520L800,520Q800,520 800,520Q800,520 800,520L800,240L320,240L320,360L640,360Q673,360 696.5,383.5Q720,407 720,440L720,520Z"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_new_window.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_new_window.xml new file mode 100644 index 000000000000..c154059f11a5 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_new_window.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 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="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + <path + android:pathData="M15 16V14H13V12.5H15V10.5H16.5V12.5H18.5V14H16.5V16H15ZM3.5 17C3.09722 17 2.74306 16.8542 2.4375 16.5625C2.14583 16.2569 2 15.9028 2 15.5V4.5C2 4.08333 2.14583 3.72917 2.4375 3.4375C2.74306 3.14583 3.09722 3 3.5 3H14.5C14.9167 3 15.2708 3.14583 15.5625 3.4375C15.8542 3.72917 16 4.08333 16 4.5V9H14.5V7H3.5V15.5H13.625V17H3.5ZM3.5 5.5H14.5V4.5H3.5V5.5ZM3.5 5.5V4.5V5.5Z" + android:fillColor="#1C1C14"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_open_in_browser.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_open_in_browser.xml new file mode 100644 index 000000000000..7d912a24c443 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_open_in_browser.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" android:tint="?attr/colorControlNormal"> + <path android:fillColor="@android:color/black" android:pathData="M160,880Q127,880 103.5,856.5Q80,833 80,800L80,440Q80,407 103.5,383.5Q127,360 160,360L240,360L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,520Q880,553 856.5,576.5Q833,600 800,600L720,600L720,800Q720,833 696.5,856.5Q673,880 640,880L160,880ZM160,800L640,800Q640,800 640,800Q640,800 640,800L640,520L160,520L160,800Q160,800 160,800Q160,800 160,800ZM720,520L800,520Q800,520 800,520Q800,520 800,520L800,240L320,240L320,360L640,360Q673,360 696.5,383.5Q720,407 720,440L720,520Z"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_dialog_background.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_dialog_background.xml index e7c89d1f9c76..f37fb8dbe118 100644 --- a/libs/WindowManager/Shell/res/drawable/letterbox_education_dialog_background.xml +++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_dialog_background.xml @@ -17,6 +17,6 @@ <shape xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:shape="rectangle"> - <solid android:color="?androidprv:attr/colorSurface"/> + <solid android:color="?androidprv:attr/materialColorSurfaceContainerHigh"/> <corners android:radius="@dimen/letterbox_education_dialog_corner_radius"/> </shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml index 72ebef625ffc..3fdd059ca982 100644 --- a/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml +++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_dismiss_button_background_ripple.xml @@ -32,7 +32,7 @@ </item> <item> <shape android:shape="rectangle"> - <solid android:color="?androidprv:attr/colorAccentPrimaryVariant"/> + <solid android:color="?androidprv:attr/materialColorPrimary"/> <corners android:radius="@dimen/letterbox_education_dialog_button_radius"/> <padding android:left="@dimen/letterbox_education_dialog_horizontal_padding" android:top="@dimen/letterbox_education_dialog_vertical_padding" diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_light_bulb.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_light_bulb.xml index 4a1e7485ed19..67929dfc5f71 100644 --- a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_light_bulb.xml +++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_light_bulb.xml @@ -18,10 +18,13 @@ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:width="@dimen/letterbox_education_dialog_title_icon_width" android:height="@dimen/letterbox_education_dialog_title_icon_height" - android:viewportWidth="45" - android:viewportHeight="44"> - - <path - android:fillColor="?androidprv:attr/colorAccentPrimaryVariant" - android:pathData="M11 40H19C19 42.2 17.2 44 15 44C12.8 44 11 42.2 11 40ZM7 38L23 38V34L7 34L7 38ZM30 19C30 26.64 24.68 30.72 22.46 32L7.54 32C5.32 30.72 0 26.64 0 19C0 10.72 6.72 4 15 4C23.28 4 30 10.72 30 19ZM26 19C26 12.94 21.06 8 15 8C8.94 8 4 12.94 4 19C4 23.94 6.98 26.78 8.7 28L21.3 28C23.02 26.78 26 23.94 26 19ZM39.74 14.74L37 16L39.74 17.26L41 20L42.26 17.26L45 16L42.26 14.74L41 12L39.74 14.74ZM35 12L36.88 7.88L41 6L36.88 4.12L35 0L33.12 4.12L29 6L33.12 7.88L35 12Z" /> + android:viewportWidth="32" + android:viewportHeight="32"> + <group> + <clip-path + android:pathData="M0,0h32v32h-32z"/> + <path + android:pathData="M5.867,22.667C4.489,21.844 3.389,20.733 2.567,19.333C1.744,17.933 1.333,16.378 1.333,14.667C1.333,12.067 2.233,9.867 4.033,8.067C5.856,6.244 8.067,5.333 10.667,5.333C13.267,5.333 15.467,6.244 17.267,8.067C19.089,9.867 20,12.067 20,14.667C20,16.378 19.589,17.933 18.767,19.333C17.944,20.733 16.844,21.844 15.467,22.667H5.867ZM6.667,20H14.667C15.511,19.356 16.167,18.578 16.633,17.667C17.1,16.733 17.333,15.733 17.333,14.667C17.333,12.822 16.678,11.256 15.367,9.967C14.078,8.656 12.511,8 10.667,8C8.822,8 7.244,8.656 5.933,9.967C4.644,11.256 4,12.822 4,14.667C4,15.733 4.233,16.733 4.7,17.667C5.167,18.578 5.822,19.356 6.667,20ZM7.2,26.667C6.822,26.667 6.5,26.544 6.233,26.3C5.989,26.033 5.867,25.711 5.867,25.333C5.867,24.956 5.989,24.644 6.233,24.4C6.5,24.133 6.822,24 7.2,24H14.133C14.511,24 14.822,24.133 15.067,24.4C15.333,24.644 15.467,24.956 15.467,25.333C15.467,25.711 15.333,26.033 15.067,26.3C14.822,26.544 14.511,26.667 14.133,26.667H7.2ZM10.667,30.667C9.933,30.667 9.3,30.411 8.767,29.9C8.256,29.367 8,28.733 8,28H13.333C13.333,28.733 13.067,29.367 12.533,29.9C12.022,30.411 11.4,30.667 10.667,30.667ZM24.667,13.367C24.667,11.7 24.078,10.278 22.9,9.1C21.722,7.922 20.3,7.333 18.633,7.333C20.3,7.333 21.722,6.756 22.9,5.6C24.078,4.422 24.667,3 24.667,1.333C24.667,3 25.244,4.422 26.4,5.6C27.578,6.756 29,7.333 30.667,7.333C29,7.333 27.578,7.922 26.4,9.1C25.244,10.278 24.667,11.7 24.667,13.367Z" + android:fillColor="?androidprv:attr/materialColorPrimary"/> + </group> </vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_reposition.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_reposition.xml index 22a8f39ca687..29e58a12f5a6 100644 --- a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_reposition.xml +++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_reposition.xml @@ -15,16 +15,16 @@ ~ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="@dimen/letterbox_education_dialog_icon_width" - android:height="@dimen/letterbox_education_dialog_icon_height" - android:viewportWidth="40" + android:width="32dp" + android:height="32dp" + android:viewportWidth="32" android:viewportHeight="32"> <path + android:pathData="M4,4C2.527,4 1.333,5.194 1.333,6.667V25.333C1.333,26.806 2.527,28 4,28H28C29.472,28 30.666,26.806 30.666,25.333V6.667C30.666,5.194 29.472,4 28,4H4ZM28,6.667H4V25.333H28V6.667Z" android:fillColor="@color/letterbox_education_text_secondary" - android:fillType="evenOdd" - android:pathData="M4 0C1.79086 0 0 1.79086 0 4V28C0 30.2091 1.79086 32 4 32H36C38.2091 32 40 30.2091 40 28V4C40 1.79086 38.2091 0 36 0H4ZM36 4H4V28H36V4Z" /> + android:fillType="evenOdd" /> <path - android:fillColor="@color/letterbox_education_text_secondary" - android:pathData="M19.98 8L17.16 10.82L20.32 14L12 14V18H20.32L17.14 21.18L19.98 24L28 16.02L19.98 8Z" /> + android:pathData="M17.32,10.667L15.44,12.547L17.546,14.667L9.333,14.667L9.333,17.333H17.546L15.426,19.453L17.32,21.333L22.666,16.013L17.32,10.667Z" + android:fillColor="@color/letterbox_education_text_secondary" /> </vector> diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_split_screen.xml b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_split_screen.xml index 15e65f716b20..6a766d37fcdb 100644 --- a/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_split_screen.xml +++ b/libs/WindowManager/Shell/res/drawable/letterbox_education_ic_split_screen.xml @@ -17,10 +17,10 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="@dimen/letterbox_education_dialog_icon_width" android:height="@dimen/letterbox_education_dialog_icon_height" - android:viewportWidth="40" - android:viewportHeight="32"> + android:viewportWidth="28" + android:viewportHeight="22"> <path android:fillColor="@color/letterbox_education_text_secondary" - android:pathData="M40 28L40 4C40 1.8 38.2 -7.86805e-08 36 -1.74846e-07L26 -6.11959e-07C23.8 -7.08124e-07 22 1.8 22 4L22 28C22 30.2 23.8 32 26 32L36 32C38.2 32 40 30.2 40 28ZM14 28L4 28L4 4L14 4L14 28ZM18 28L18 4C18 1.8 16.2 -1.04033e-06 14 -1.1365e-06L4 -1.57361e-06C1.8 -1.66978e-06 -7.86805e-08 1.8 -1.74846e-07 4L-1.22392e-06 28C-1.32008e-06 30.2 1.8 32 4 32L14 32C16.2 32 18 30.2 18 28Z" /> + android:pathData="M27.333,19L27.333,3C27.333,1.533 26.133,0.333 24.666,0.333L18,0.333C16.533,0.333 15.333,1.533 15.333,3L15.333,19C15.333,20.467 16.533,21.667 18,21.667L24.666,21.667C26.133,21.667 27.333,20.467 27.333,19ZM10,19L3.333,19L3.333,3L10,3L10,19ZM12.666,19L12.666,3C12.666,1.533 11.466,0.333 10,0.333L3.333,0.333C1.866,0.333 0.666,1.533 0.666,3L0.666,19C0.666,20.467 1.866,21.667 3.333,21.667L10,21.667C11.466,21.667 12.666,20.467 12.666,19Z" /> </vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_button_background_ripple.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_button_background_ripple.xml index 1f125148775d..4207482260ba 100644 --- a/libs/WindowManager/Shell/res/drawable/letterbox_restart_button_background_ripple.xml +++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_button_background_ripple.xml @@ -32,7 +32,7 @@ </item> <item> <shape android:shape="rectangle"> - <solid android:color="?androidprv:attr/colorAccentPrimaryVariant"/> + <solid android:color="?androidprv:attr/materialColorPrimary"/> <corners android:radius="@dimen/letterbox_restart_dialog_button_radius"/> <padding android:left="@dimen/letterbox_restart_dialog_horizontal_padding" android:top="@dimen/letterbox_restart_dialog_vertical_padding" diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_dialog_background.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_dialog_background.xml index e3c18a2db66f..72cfeefceffb 100644 --- a/libs/WindowManager/Shell/res/drawable/letterbox_restart_dialog_background.xml +++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_dialog_background.xml @@ -17,6 +17,6 @@ <shape xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:shape="rectangle"> - <solid android:color="?androidprv:attr/colorSurface"/> + <solid android:color="?androidprv:attr/materialColorSurfaceContainerHigh"/> <corners android:radius="@dimen/letterbox_restart_dialog_corner_radius"/> </shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_dismiss_button_background_ripple.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_dismiss_button_background_ripple.xml index 3aa0981e45aa..816b35063b00 100644 --- a/libs/WindowManager/Shell/res/drawable/letterbox_restart_dismiss_button_background_ripple.xml +++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_dismiss_button_background_ripple.xml @@ -32,9 +32,9 @@ </item> <item> <shape android:shape="rectangle"> - <stroke android:color="?androidprv:attr/colorAccentPrimaryVariant" + <stroke android:color="?androidprv:attr/materialColorOutlineVariant" android:width="1dp"/> - <solid android:color="?androidprv:attr/colorSurface"/> + <solid android:color="?androidprv:attr/materialColorSurfaceContainerHigh"/> <corners android:radius="@dimen/letterbox_restart_dialog_button_radius"/> <padding android:left="@dimen/letterbox_restart_dialog_horizontal_padding" android:top="@dimen/letterbox_restart_dialog_vertical_padding" diff --git a/libs/WindowManager/Shell/res/drawable/letterbox_restart_header_ic_arrows.xml b/libs/WindowManager/Shell/res/drawable/letterbox_restart_header_ic_arrows.xml index 5053971a17d3..f13d26c7f89e 100644 --- a/libs/WindowManager/Shell/res/drawable/letterbox_restart_header_ic_arrows.xml +++ b/libs/WindowManager/Shell/res/drawable/letterbox_restart_header_ic_arrows.xml @@ -18,15 +18,13 @@ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:width="@dimen/letterbox_restart_dialog_title_icon_width" android:height="@dimen/letterbox_restart_dialog_title_icon_height" - android:viewportWidth="45" - android:viewportHeight="44"> - <group - android:scaleX="0.8" - android:scaleY="0.8" - android:translateX="8" - android:translateY="8"> + android:viewportWidth="32" + android:viewportHeight="32"> + <group> + <clip-path + android:pathData="M0,0h32v32h-32z"/> <path - android:pathData="M0,36V24.5H3V30.85L10.4,23.45L12.55,25.6L5.15,33H11.5V36H0ZM24.5,36V33H30.85L23.5,25.65L25.65,23.5L33,30.85V24.5H36V36H24.5ZM10.35,12.5L3,5.15V11.5H0V0H11.5V3H5.15L12.5,10.35L10.35,12.5ZM25.65,12.5L23.5,10.35L30.85,3H24.5V0H36V11.5H33V5.15L25.65,12.5Z" - android:fillColor="?androidprv:attr/colorAccentPrimaryVariant"/> + android:pathData="M8.533,25.333H10.667C11.044,25.333 11.356,25.467 11.6,25.733C11.867,25.978 12,26.289 12,26.667C12,27.044 11.867,27.367 11.6,27.633C11.356,27.878 11.044,28 10.667,28H5.333C4.956,28 4.633,27.878 4.367,27.633C4.122,27.367 4,27.044 4,26.667V21.333C4,20.956 4.122,20.644 4.367,20.4C4.633,20.133 4.956,20 5.333,20C5.711,20 6.022,20.133 6.267,20.4C6.533,20.644 6.667,20.956 6.667,21.333V23.467L9.867,20.267C10.111,20.022 10.422,19.9 10.8,19.9C11.178,19.9 11.489,20.022 11.733,20.267C11.978,20.511 12.1,20.822 12.1,21.2C12.1,21.578 11.978,21.889 11.733,22.133L8.533,25.333ZM23.467,25.333L20.267,22.133C20.022,21.889 19.9,21.578 19.9,21.2C19.9,20.822 20.022,20.511 20.267,20.267C20.511,20.022 20.822,19.9 21.2,19.9C21.578,19.9 21.889,20.022 22.133,20.267L25.333,23.467V21.333C25.333,20.956 25.456,20.644 25.7,20.4C25.967,20.133 26.289,20 26.667,20C27.044,20 27.356,20.133 27.6,20.4C27.867,20.644 28,20.956 28,21.333V26.667C28,27.044 27.867,27.367 27.6,27.633C27.356,27.878 27.044,28 26.667,28H21.333C20.956,28 20.633,27.878 20.367,27.633C20.122,27.367 20,27.044 20,26.667C20,26.289 20.122,25.978 20.367,25.733C20.633,25.467 20.956,25.333 21.333,25.333H23.467ZM6.667,8.533V10.667C6.667,11.044 6.533,11.367 6.267,11.633C6.022,11.878 5.711,12 5.333,12C4.956,12 4.633,11.878 4.367,11.633C4.122,11.367 4,11.044 4,10.667V5.333C4,4.956 4.122,4.644 4.367,4.4C4.633,4.133 4.956,4 5.333,4H10.667C11.044,4 11.356,4.133 11.6,4.4C11.867,4.644 12,4.956 12,5.333C12,5.711 11.867,6.033 11.6,6.3C11.356,6.544 11.044,6.667 10.667,6.667H8.533L11.733,9.867C11.978,10.111 12.1,10.422 12.1,10.8C12.1,11.178 11.978,11.489 11.733,11.733C11.489,11.978 11.178,12.1 10.8,12.1C10.422,12.1 10.111,11.978 9.867,11.733L6.667,8.533ZM25.333,8.533L22.133,11.733C21.889,11.978 21.578,12.1 21.2,12.1C20.822,12.1 20.511,11.978 20.267,11.733C20.022,11.489 19.9,11.178 19.9,10.8C19.9,10.422 20.022,10.111 20.267,9.867L23.467,6.667H21.333C20.956,6.667 20.633,6.544 20.367,6.3C20.122,6.033 20,5.711 20,5.333C20,4.956 20.122,4.644 20.367,4.4C20.633,4.133 20.956,4 21.333,4H26.667C27.044,4 27.356,4.133 27.6,4.4C27.867,4.644 28,4.956 28,5.333V10.667C28,11.044 27.867,11.367 27.6,11.633C27.356,11.878 27.044,12 26.667,12C26.289,12 25.967,11.878 25.7,11.633C25.456,11.367 25.333,11.044 25.333,10.667V8.533Z" + android:fillColor="?androidprv:attr/materialColorPrimary"/> </group> </vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml index 34f03c2f226b..501bedd50f55 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml @@ -19,7 +19,7 @@ android:layout_height="wrap_content" android:layout_width="wrap_content" android:orientation="vertical" - android:id="@+id/bubble_bar_expanded_view"> + android:id="@+id/bubble_expanded_view"> <com.android.wm.shell.bubbles.bar.BubbleBarHandleView android:id="@+id/bubble_bar_handle_view" diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml index a0a06f1b3721..806d026a7e7c 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> -<com.android.wm.shell.common.bubbles.BubblePopupView +<com.android.wm.shell.shared.bubbles.BubblePopupView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -53,4 +53,4 @@ android:textAlignment="center" android:text="@string/bubble_bar_education_manage_text"/> -</com.android.wm.shell.common.bubbles.BubblePopupView>
\ No newline at end of file +</com.android.wm.shell.shared.bubbles.BubblePopupView>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_item.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_item.xml index ddcd5c60d9c8..e3217811ca29 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_item.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_item.xml @@ -16,6 +16,7 @@ --> <com.android.wm.shell.bubbles.bar.BubbleBarMenuItemView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="@dimen/bubble_bar_manage_menu_item_height" @@ -35,7 +36,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" - android:textColor="?android:attr/textColorPrimary" + android:textColor="?androidprv:attr/materialColorOnSurface" android:textAppearance="@*android:style/TextAppearance.DeviceDefault" /> </com.android.wm.shell.bubbles.bar.BubbleBarMenuItemView>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml index 82e5aee41ff2..f1ecde49ce78 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml @@ -17,6 +17,7 @@ <com.android.wm.shell.bubbles.bar.BubbleBarMenuView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" @@ -51,7 +52,7 @@ android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_weight="1" - android:textColor="?android:attr/textColorPrimary" + android:textColor="?androidprv:attr/materialColorOnSurface" android:textAppearance="@*android:style/TextAppearance.DeviceDefault" /> <ImageView @@ -61,7 +62,7 @@ android:layout_marginStart="8dp" android:contentDescription="@null" android:src="@drawable/ic_expand_less" - app:tint="?android:attr/textColorPrimary" /> + app:tint="?androidprv:attr/materialColorOnSurface" /> </LinearLayout> @@ -71,6 +72,8 @@ android:layout_height="wrap_content" android:orientation="vertical" android:layout_marginTop="@dimen/bubble_bar_manage_menu_section_spacing" + android:clipChildren="true" + android:clipToOutline="true" android:background="@drawable/bubble_manage_menu_bg" android:elevation="@dimen/bubble_manage_menu_elevation" /> diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml index b489a5c1acd0..7fa586c626be 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> -<com.android.wm.shell.common.bubbles.BubblePopupView +<com.android.wm.shell.shared.bubbles.BubblePopupView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -53,4 +53,4 @@ android:textAlignment="center" android:text="@string/bubble_bar_education_stack_text"/> -</com.android.wm.shell.common.bubbles.BubblePopupView>
\ No newline at end of file +</com.android.wm.shell.shared.bubbles.BubblePopupView>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml index 257fe1544bbb..62782a784db9 100644 --- a/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml +++ b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml @@ -21,38 +21,6 @@ android:orientation="vertical" android:gravity="bottom|end"> - <include android:id="@+id/camera_compat_hint" - android:visibility="gone" - android:layout_width="@dimen/camera_compat_hint_width" - android:layout_height="wrap_content" - layout="@layout/compat_mode_hint"/> - - <LinearLayout - android:id="@+id/camera_compat_control" - android:visibility="gone" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:clipToPadding="false" - android:layout_marginEnd="@dimen/compat_button_margin" - android:layout_marginBottom="@dimen/compat_button_margin" - android:orientation="vertical"> - - <ImageButton - android:id="@+id/camera_compat_treatment_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:background="@android:color/transparent"/> - - <ImageButton - android:id="@+id/camera_compat_dismiss_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:src="@drawable/camera_compat_dismiss_ripple" - android:background="@android:color/transparent" - android:contentDescription="@string/camera_compat_dismiss_button_description"/> - - </LinearLayout> - <include android:id="@+id/size_compat_hint" android:visibility="gone" android:layout_width="@dimen/compat_hint_width" diff --git a/libs/WindowManager/Shell/res/layout/compat_ui_restart_button_layout.xml b/libs/WindowManager/Shell/res/layout/compat_ui_restart_button_layout.xml new file mode 100644 index 000000000000..d00c69cb2993 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/compat_ui_restart_button_layout.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="bottom|end"> + + <include android:id="@+id/size_compat_hint" + android:visibility="gone" + android:layout_width="@dimen/compat_hint_width" + android:layout_height="wrap_content" + layout="@layout/compat_mode_hint"/> + + <ImageButton + android:id="@+id/size_compat_restart_button" + android:visibility="gone" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/compat_button_margin" + android:layout_marginBottom="@dimen/compat_button_margin" + android:src="@drawable/size_compat_restart_button_ripple" + android:background="@android:color/transparent" + android:contentDescription="@string/restart_button_description"/> + +</LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml index c0ff1922edc8..1d1cdfa85040 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml @@ -28,6 +28,8 @@ android:layout_height="@dimen/desktop_mode_fullscreen_decor_caption_height" android:paddingVertical="16dp" android:paddingHorizontal="10dp" + android:screenReaderFocusable="true" + android:importantForAccessibility="yes" android:contentDescription="@string/handle_text" android:src="@drawable/decor_handle_dark" tools:tint="@color/desktop_mode_caption_handle_bar_dark" diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml index 7b31c1420a7c..3dbf7542ac6e 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml @@ -31,14 +31,16 @@ android:orientation="horizontal" android:clickable="true" android:focusable="true" + android:contentDescription="@string/desktop_mode_app_header_chip_text" android:layout_marginStart="12dp"> <ImageView android:id="@+id/application_icon" android:layout_width="@dimen/desktop_mode_caption_icon_radius" android:layout_height="@dimen/desktop_mode_caption_icon_radius" android:layout_gravity="center_vertical" - android:contentDescription="@string/app_icon_text" android:layout_marginStart="6dp" + android:clickable="false" + android:focusable="false" android:scaleType="centerCrop"/> <TextView @@ -53,18 +55,22 @@ android:layout_gravity="center_vertical" android:layout_weight="1" android:layout_marginStart="8dp" + android:clickable="false" + android:focusable="false" tools:text="Gmail"/> <ImageButton android:id="@+id/expand_menu_button" android:layout_width="16dp" android:layout_height="16dp" - android:contentDescription="@string/expand_menu_text" android:src="@drawable/ic_baseline_expand_more_24" android:background="@null" android:scaleType="fitCenter" android:clickable="false" android:focusable="false" + android:screenReaderFocusable="false" + android:importantForAccessibility="no" + android:contentDescription="@null" android:layout_marginHorizontal="8dp" android:layout_gravity="center_vertical"/> @@ -76,8 +82,21 @@ android:layout_height="40dp" android:layout_weight="1"/> + <ImageButton + android:id="@+id/minimize_window" + android:layout_width="44dp" + android:layout_height="40dp" + android:paddingHorizontal="10dp" + android:paddingVertical="8dp" + android:layout_marginEnd="8dp" + android:contentDescription="@string/minimize_button_text" + android:src="@drawable/desktop_mode_header_ic_minimize" + android:scaleType="centerCrop" + android:gravity="end"/> + <com.android.wm.shell.windowdecor.MaximizeButtonView android:id="@+id/maximize_button_view" + android:importantForAccessibility="no" android:layout_width="44dp" android:layout_height="40dp" android:layout_gravity="end" diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml index d5724cc6a420..6913e54c2b10 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml @@ -17,8 +17,13 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/handle_menu" android:layout_width="@dimen/desktop_mode_handle_menu_width" android:layout_height="wrap_content" + android:clipChildren="false" + android:clipToPadding="false" + android:paddingBottom="@dimen/desktop_mode_handle_menu_pill_elevation" + android:paddingRight="@dimen/desktop_mode_handle_menu_pill_elevation" android:orientation="vertical"> <LinearLayout @@ -27,7 +32,7 @@ android:layout_height="@dimen/desktop_mode_handle_menu_app_info_pill_height" android:layout_marginTop="@dimen/desktop_mode_handle_menu_margin_top" android:layout_marginStart="1dp" - android:elevation="1dp" + android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation" android:orientation="horizontal" android:background="@drawable/desktop_mode_decor_handle_menu_background" android:gravity="center_vertical"> @@ -38,13 +43,15 @@ android:layout_height="@dimen/desktop_mode_caption_icon_radius" android:layout_marginStart="12dp" android:layout_marginEnd="12dp" - android:contentDescription="@string/app_icon_text"/> + android:contentDescription="@string/app_icon_text" + android:importantForAccessibility="no"/> <TextView android:id="@+id/application_name" android:layout_width="0dp" android:layout_height="wrap_content" tools:text="Gmail" + android:importantForAccessibility="no" android:textColor="?androidprv:attr/materialColorOnSurface" android:textSize="14sp" android:textFontWeight="500" @@ -73,7 +80,7 @@ android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin" android:layout_marginStart="1dp" android:orientation="horizontal" - android:elevation="1dp" + android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation" android:background="@drawable/desktop_mode_decor_handle_menu_background" android:gravity="center_vertical"> @@ -120,11 +127,11 @@ <LinearLayout android:id="@+id/more_actions_pill" android:layout_width="match_parent" - android:layout_height="@dimen/desktop_mode_handle_menu_more_actions_pill_height" + android:layout_height="wrap_content" android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin" android:layout_marginStart="1dp" android:orientation="vertical" - android:elevation="1dp" + android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation" android:background="@drawable/desktop_mode_decor_handle_menu_background"> <Button @@ -134,6 +141,41 @@ android:drawableStart="@drawable/desktop_mode_ic_handle_menu_screenshot" android:drawableTint="?androidprv:attr/materialColorOnSurface" style="@style/DesktopModeHandleMenuActionButton"/> + + <Button + android:id="@+id/new_window_button" + android:contentDescription="@string/new_window_text" + android:text="@string/new_window_text" + android:drawableStart="@drawable/desktop_mode_ic_handle_menu_new_window" + android:drawableTint="?androidprv:attr/materialColorOnSurface" + style="@style/DesktopModeHandleMenuActionButton" /> + + <Button + android:id="@+id/manage_windows_button" + android:contentDescription="@string/manage_windows_text" + android:text="@string/manage_windows_text" + android:drawableStart="@drawable/desktop_mode_ic_handle_menu_manage_windows" + android:drawableTint="?androidprv:attr/materialColorOnSurface" + style="@style/DesktopModeHandleMenuActionButton" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/open_in_browser_pill" + android:layout_width="match_parent" + android:layout_height="@dimen/desktop_mode_handle_menu_open_in_browser_pill_height" + android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin" + android:layout_marginStart="1dp" + android:orientation="vertical" + android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation" + android:background="@drawable/desktop_mode_decor_handle_menu_background"> + + <Button + android:id="@+id/open_in_browser_button" + android:contentDescription="@string/open_in_browser_text" + android:text="@string/open_in_browser_text" + android:drawableStart="@drawable/desktop_mode_ic_handle_menu_open_in_browser" + android:drawableTint="?androidprv:attr/materialColorOnSurface" + style="@style/DesktopModeHandleMenuActionButton"/> </LinearLayout> </LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml index 7d5f9cdbebc8..35ef2393bb9b 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml @@ -14,88 +14,108 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/maximize_menu" - style="?android:attr/buttonBarStyle" android:layout_width="@dimen/desktop_mode_maximize_menu_width" android:layout_height="@dimen/desktop_mode_maximize_menu_height" - android:orientation="horizontal" - android:gravity="center" - android:padding="16dp" android:background="@drawable/desktop_mode_maximize_menu_background" android:elevation="1dp"> <LinearLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical"> + android:id="@+id/container" + android:layout_width="@dimen/desktop_mode_maximize_menu_width" + android:layout_height="@dimen/desktop_mode_maximize_menu_height" + android:orientation="horizontal" + android:padding="16dp" + android:gravity="center"> - <Button - android:layout_width="94dp" - android:layout_height="60dp" - android:id="@+id/maximize_menu_maximize_button" - style="?android:attr/buttonBarButtonStyle" - android:stateListAnimator="@null" - android:layout_marginRight="8dp" - android:layout_marginBottom="4dp" - android:alpha="0"/> - - <TextView - android:id="@+id/maximize_menu_maximize_window_text" - android:layout_width="94dp" - android:layout_height="18dp" - android:textSize="11sp" - android:layout_marginBottom="76dp" - android:gravity="center" - android:fontFamily="google-sans-text" - android:text="@string/desktop_mode_maximize_menu_maximize_text" - android:textColor="?androidprv:attr/materialColorOnSurface" - android:alpha="0"/> - </LinearLayout> - - <LinearLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical"> <LinearLayout - android:id="@+id/maximize_menu_snap_menu_layout" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="horizontal" - android:padding="4dp" - android:background="@drawable/desktop_mode_maximize_menu_layout_background" - android:layout_marginBottom="4dp" - android:alpha="0"> - <Button - android:id="@+id/maximize_menu_snap_left_button" - style="?android:attr/buttonBarButtonStyle" - android:layout_width="41dp" - android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" - android:layout_marginRight="4dp" - android:background="@drawable/desktop_mode_maximize_menu_button_background" - android:stateListAnimator="@null"/> + android:orientation="vertical"> <Button - android:id="@+id/maximize_menu_snap_right_button" + android:layout_width="94dp" + android:layout_height="60dp" + android:id="@+id/maximize_menu_maximize_button" style="?android:attr/buttonBarButtonStyle" - android:layout_width="41dp" - android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" - android:background="@drawable/desktop_mode_maximize_menu_button_background" - android:stateListAnimator="@null"/> + android:stateListAnimator="@null" + android:importantForAccessibility="yes" + android:contentDescription="@string/desktop_mode_maximize_menu_maximize_button_text" + android:layout_marginRight="8dp" + android:layout_marginBottom="4dp" + android:alpha="0"/> + + <TextView + android:id="@+id/maximize_menu_maximize_window_text" + android:layout_width="94dp" + android:layout_height="18dp" + android:textSize="11sp" + android:layout_marginBottom="76dp" + android:gravity="center" + android:fontFamily="google-sans-text" + android:importantForAccessibility="no" + android:text="@string/desktop_mode_maximize_menu_maximize_text" + android:textColor="?androidprv:attr/materialColorOnSurface" + android:alpha="0"/> + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + <LinearLayout + android:id="@+id/maximize_menu_snap_menu_layout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="4dp" + android:background="@drawable/desktop_mode_maximize_menu_layout_background" + android:layout_marginBottom="4dp" + android:alpha="0"> + <Button + android:id="@+id/maximize_menu_snap_left_button" + style="?android:attr/buttonBarButtonStyle" + android:layout_width="41dp" + android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" + android:layout_marginRight="4dp" + android:background="@drawable/desktop_mode_maximize_menu_button_background" + android:importantForAccessibility="yes" + android:contentDescription="@string/desktop_mode_maximize_menu_snap_left_button_text" + android:stateListAnimator="@null"/> + + <Button + android:id="@+id/maximize_menu_snap_right_button" + style="?android:attr/buttonBarButtonStyle" + android:layout_width="41dp" + android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" + android:background="@drawable/desktop_mode_maximize_menu_button_background" + android:importantForAccessibility="yes" + android:contentDescription="@string/desktop_mode_maximize_menu_snap_right_button_text" + android:stateListAnimator="@null"/> + </LinearLayout> + <TextView + android:id="@+id/maximize_menu_snap_window_text" + android:layout_width="94dp" + android:layout_height="18dp" + android:textSize="11sp" + android:layout_marginBottom="76dp" + android:layout_gravity="center" + android:gravity="center" + android:importantForAccessibility="no" + android:fontFamily="google-sans-text" + android:text="@string/desktop_mode_maximize_menu_snap_text" + android:textColor="?androidprv:attr/materialColorOnSurface" + android:alpha="0"/> </LinearLayout> - <TextView - android:id="@+id/maximize_menu_snap_window_text" - android:layout_width="94dp" - android:layout_height="18dp" - android:textSize="11sp" - android:layout_marginBottom="76dp" - android:layout_gravity="center" - android:gravity="center" - android:fontFamily="google-sans-text" - android:text="@string/desktop_mode_maximize_menu_snap_text" - android:textColor="?androidprv:attr/materialColorOnSurface" - android:alpha="0"/> </LinearLayout> -</LinearLayout> + + <!-- Empty view intentionally placed in front of everything else and matching the menu size + used to monitor input events over the entire menu. --> + <View + android:id="@+id/maximize_menu_overlay" + android:layout_width="@dimen/desktop_mode_maximize_menu_width" + android:layout_height="@dimen/desktop_mode_maximize_menu_height"/> +</FrameLayout> diff --git a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml index c77a4fdcfa79..bda087b143d0 100644 --- a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml +++ b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_action_layout.xml @@ -24,10 +24,10 @@ <ImageView android:id="@+id/letterbox_education_dialog_action_icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_width="32dp" + android:layout_height="32dp" android:layout_gravity="center" - android:layout_marginBottom="20dp"/> + android:layout_marginBottom="12dp"/> <TextView android:fontFamily="@*android:string/config_bodyFontFamily" @@ -37,7 +37,7 @@ android:layout_height="wrap_content" android:lineSpacingExtra="4sp" android:textAlignment="center" - android:textColor="?android:attr/textColorSecondary" + android:textColor="?androidprv:attr/materialColorOnSurface" android:textSize="14sp"/> </LinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml index 4d5256777018..488123ad7b0c 100644 --- a/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml +++ b/libs/WindowManager/Shell/res/layout/letterbox_education_dialog_layout.xml @@ -45,19 +45,16 @@ android:layout_height="wrap_content"> <LinearLayout + android:padding="24dp" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" - android:orientation="vertical" - android:paddingTop="32dp" - android:paddingBottom="32dp" - android:paddingLeft="56dp" - android:paddingRight="56dp"> + android:orientation="vertical"> <ImageView android:layout_width="@dimen/letterbox_education_dialog_title_icon_width" android:layout_height="@dimen/letterbox_education_dialog_title_icon_height" - android:layout_marginBottom="17dp" + android:layout_marginBottom="16dp" android:src="@drawable/letterbox_education_ic_light_bulb"/> <TextView @@ -67,9 +64,8 @@ android:lineSpacingExtra="4sp" android:text="@string/letterbox_education_dialog_title" android:textAlignment="center" - android:textColor="?android:attr/textColorPrimary" - android:fontFamily="@*android:string/config_bodyFontFamilyMedium" - android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Headline" + android:textColor="?androidprv:attr/materialColorOnSurface" + android:fontFamily="@*android:string/config_headlineFontFamily" android:textSize="24sp"/> <LinearLayout @@ -77,7 +73,8 @@ android:layout_height="wrap_content" android:gravity="top" android:orientation="horizontal" - android:paddingTop="48dp"> + android:layout_marginHorizontal="18dp" + android:layout_marginVertical="@dimen/letterbox_education_dialog_margin"> <com.android.wm.shell.compatui.LetterboxEduDialogActionLayout android:layout_width="wrap_content" @@ -101,15 +98,13 @@ android:lineHeight="20dp" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Small" android:id="@+id/letterbox_education_dialog_dismiss_button" - android:textStyle="bold" android:layout_width="match_parent" android:layout_height="56dp" - android:layout_marginTop="40dp" android:textSize="14sp" android:background= "@drawable/letterbox_education_dismiss_button_background_ripple" android:text="@string/letterbox_education_got_it" - android:textColor="?android:attr/textColorPrimaryInverse" + android:textColor="?androidprv:attr/materialColorOnPrimary" android:textAlignment="center" android:contentDescription="@string/letterbox_education_got_it"/> diff --git a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml index 7f1aac3551b6..462a49ccb1eb 100644 --- a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml +++ b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml @@ -70,26 +70,27 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/letterbox_restart_dialog_description" - android:textAlignment="center"/> + android:gravity="start"/> <LinearLayout android:id="@+id/letterbox_restart_dialog_checkbox_container" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingVertical="14dp" + android:paddingVertical="16dp" android:orientation="horizontal" android:layout_gravity="center_vertical" - android:layout_marginVertical="18dp"> + android:layout_marginVertical="16dp"> <CheckBox android:id="@+id/letterbox_restart_dialog_checkbox" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginStart="12dp" android:button="@drawable/letterbox_restart_checkbox_button"/> <TextView android:textAppearance="@style/RestartDialogCheckboxText" - android:layout_marginStart="12dp" + android:layout_marginStart="20dp" android:id="@+id/letterbox_restart_dialog_checkbox_description" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -98,19 +99,22 @@ </LinearLayout> - <FrameLayout + + <LinearLayout android:minHeight="@dimen/letterbox_restart_dialog_button_height" - android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end"> <Button android:textAppearance="@style/RestartDialogDismissButton" android:id="@+id/letterbox_restart_dialog_dismiss_button" + style="?android:attr/buttonBarButtonStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginRight="8dp" android:minWidth="@dimen/letterbox_restart_dialog_button_width" android:minHeight="@dimen/letterbox_restart_dialog_button_height" - android:layout_gravity="start" android:background= "@drawable/letterbox_restart_dismiss_button_background_ripple" android:text="@string/letterbox_restart_cancel" @@ -119,17 +123,17 @@ <Button android:textAppearance="@style/RestartDialogConfirmButton" android:id="@+id/letterbox_restart_dialog_restart_button" + style="?android:attr/buttonBarButtonStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:minWidth="@dimen/letterbox_restart_dialog_button_width" android:minHeight="@dimen/letterbox_restart_dialog_button_height" - android:layout_gravity="end" android:background= "@drawable/letterbox_restart_button_background_ripple" android:text="@string/letterbox_restart_restart" android:contentDescription="@string/letterbox_restart_restart"/> - </FrameLayout> + </LinearLayout> </LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml index cf1b8947467e..b734d2d81455 100644 --- a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml +++ b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml @@ -19,7 +19,8 @@ <FrameLayout android:layout_width="44dp" - android:layout_height="40dp"> + android:layout_height="40dp" + android:importantForAccessibility="noHideDescendants"> <ProgressBar android:id="@+id/progress_bar" style="?android:attr/progressBarStyleHorizontal" diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml index 8dcbc46570ee..8c74c5026626 100644 --- a/libs/WindowManager/Shell/res/values-af/strings.xml +++ b/libs/WindowManager/Shell/res/values-af/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Beweeg na regs bo"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Beweeg na links onder"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Beweeg na regs onder"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"vou kieslys uit"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"vou kieslys in"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Skuif links"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Skuif regs"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"vou <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> uit"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"vou <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> in"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>-instellings"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Maak borrel toe"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Gaan na volskerm"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Moenie dat gesprek \'n borrel word nie"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Klets met borrels"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nuwe gesprekke verskyn as swerwende ikone, of borrels Tik op borrel om dit oop te maak. Sleep om dit te skuif."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Maak klein"</string> <string name="close_button_text" msgid="2913281996024033299">"Maak toe"</string> <string name="back_button_text" msgid="1469718707134137085">"Terug"</string> - <string name="handle_text" msgid="1766582106752184456">"Handvatsel"</string> + <string name="handle_text" msgid="4419667835599523257">"Apphandvatsel"</string> <string name="app_icon_text" msgid="2823268023931811747">"Appikoon"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Volskerm"</string> <string name="desktop_text" msgid="1077633567027630454">"Rekenaarmodus"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Sweef"</string> <string name="select_text" msgid="5139083974039906583">"Kies"</string> <string name="screenshot_text" msgid="1477704010087786671">"Skermskoot"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Maak in blaaier oop"</string> + <string name="new_window_text" msgid="6318648868380652280">"Nuwe venster"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Bestuur vensters"</string> <string name="close_text" msgid="4986518933445178928">"Maak toe"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Maak kieslys toe"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Maak kieslys oop"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Maak kieslys oop"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimeer skerm"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Gryp skerm vas"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Hierdie app se grootte kan nie verander word nie"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimeer"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Spring na links"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Spring na regs"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml index d03a5ef42a4d..58b0a88b9f6c 100644 --- a/libs/WindowManager/Shell/res/values-am/strings.xml +++ b/libs/WindowManager/Shell/res/values-am/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ወደ ላይኛው ቀኝ አንቀሳቅስ"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"የግርጌውን ግራ አንቀሳቅስ"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ታችኛውን ቀኝ ያንቀሳቅሱ"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"ምናሌን ዘርጋ"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"ምናሌን ሰብስብ"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"ወደ ግራ ውሰድ"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"ወደ ቀኝ ውሰድ"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>ን ዘርጋ"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>ን ሰብስብ"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"የ<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ቅንብሮች"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"አረፋን አሰናብት"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"ወደ ሙሉ ማያ ገፅ ያንቀሳቅሱ"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"ውይይቶችን በአረፋ አታሳይ"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"አረፋዎችን በመጠቀም ይወያዩ"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"አዲስ ውይይቶች እንደ ተንሳፋፊ አዶዎች ወይም አረፋዎች ሆነው ይታያሉ። አረፋን ለመክፈት መታ ያድርጉ። ለመውሰድ ይጎትቱት።"</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"አሳንስ"</string> <string name="close_button_text" msgid="2913281996024033299">"ዝጋ"</string> <string name="back_button_text" msgid="1469718707134137085">"ተመለስ"</string> - <string name="handle_text" msgid="1766582106752184456">"መያዣ"</string> + <string name="handle_text" msgid="4419667835599523257">"የመተግበሪያ መያዣ"</string> <string name="app_icon_text" msgid="2823268023931811747">"የመተግበሪያ አዶ"</string> <string name="fullscreen_text" msgid="1162316685217676079">"ሙሉ ማያ"</string> <string name="desktop_text" msgid="1077633567027630454">"የዴስክቶፕ ሁነታ"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"ተንሳፋፊ"</string> <string name="select_text" msgid="5139083974039906583">"ምረጥ"</string> <string name="screenshot_text" msgid="1477704010087786671">"ቅጽበታዊ ገፅ ዕይታ"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"በአሳሽ ውስጥ ክፈት"</string> + <string name="new_window_text" msgid="6318648868380652280">"አዲስ መስኮት"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"መስኮቶችን አስተዳድር"</string> <string name="close_text" msgid="4986518933445178928">"ዝጋ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ምናሌ ዝጋ"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"ምናሌን ክፈት"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"ምናሌን ክፈት"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"የማያ ገጹ መጠን አሳድግ"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ማያ ገጹን አሳድግ"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"ይህ መተግበሪያ መጠኑ ሊቀየር አይችልም"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"አሳድግ"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ወደ ግራ አሳድግ"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"ወደ ቀኝ አሳድግ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml index 2ff449bd781b..c23a57dbf312 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"الانتقال إلى أعلى اليسار"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"نقل إلى أسفل يمين الشاشة"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"نقل إلى أسفل اليسار"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"توسيع القائمة"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"تصغير القائمة"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"نقل لليسار"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"نقل لليمين"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"توسيع <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"تصغير <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"إعدادات <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"إغلاق فقاعة المحادثة"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"الانتقال إلى وضع ملء الشاشة"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"عدم عرض المحادثة كفقاعة محادثة"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"الدردشة باستخدام فقاعات المحادثات"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"تظهر المحادثات الجديدة كرموز عائمة أو كفقاعات. انقر لفتح فقاعة المحادثة، واسحبها لتحريكها."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"تصغير"</string> <string name="close_button_text" msgid="2913281996024033299">"إغلاق"</string> <string name="back_button_text" msgid="1469718707134137085">"رجوع"</string> - <string name="handle_text" msgid="1766582106752184456">"مقبض"</string> + <string name="handle_text" msgid="4419667835599523257">"مقبض التطبيق"</string> <string name="app_icon_text" msgid="2823268023931811747">"رمز التطبيق"</string> <string name="fullscreen_text" msgid="1162316685217676079">"ملء الشاشة"</string> <string name="desktop_text" msgid="1077633567027630454">"وضع سطح المكتب"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"نافذة عائمة"</string> <string name="select_text" msgid="5139083974039906583">"اختيار"</string> <string name="screenshot_text" msgid="1477704010087786671">"لقطة شاشة"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"فتح في المتصفِّح"</string> + <string name="new_window_text" msgid="6318648868380652280">"نافذة جديدة"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"إدارة النوافذ"</string> <string name="close_text" msgid="4986518933445178928">"إغلاق"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"إغلاق القائمة"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"فتح القائمة"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"فتح القائمة"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"تكبير الشاشة إلى أقصى حدّ"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"التقاط صورة للشاشة"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"لا يمكن تغيير حجم نافذة هذا التطبيق"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"تكبير"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"المحاذاة إلى اليسار"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"المحاذاة إلى اليمين"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml index 04a3e5cdf90d..e0acfd795cfc 100644 --- a/libs/WindowManager/Shell/res/values-as/strings.xml +++ b/libs/WindowManager/Shell/res/values-as/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"শীৰ্ষৰ সোঁফালে নিয়ক"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"বুটামটো বাওঁফালে নিয়ক"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"তলৰ সোঁফালে নিয়ক"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"মেনু বিস্তাৰ কৰক"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"মেনু সংকোচন কৰক"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"বাওঁফাললৈ নিয়ক"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"সোঁফাললৈ নিয়ক"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> বিস্তাৰ কৰক"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> সংকোচন কৰক"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ছেটিং"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"বাবল অগ্ৰাহ্য কৰক"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"পূৰ্ণ স্ক্ৰীনলৈ নিয়ক"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"বাৰ্তালাপ বাবল নকৰিব"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Bubbles ব্যৱহাৰ কৰি চাট কৰক"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"নতুন বাৰ্তালাপ উপঙি থকা চিহ্নসমূহ অথবা bubbles হিচাপে প্ৰদর্শিত হয়। Bubbles খুলিবলৈ টিপক। এইটো স্থানান্তৰ কৰিবলৈ টানি নিয়ক।"</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"মিনিমাইজ কৰক"</string> <string name="close_button_text" msgid="2913281996024033299">"বন্ধ কৰক"</string> <string name="back_button_text" msgid="1469718707134137085">"উভতি যাওক"</string> - <string name="handle_text" msgid="1766582106752184456">"হেণ্ডেল"</string> + <string name="handle_text" msgid="4419667835599523257">"এপৰ হেণ্ডেল"</string> <string name="app_icon_text" msgid="2823268023931811747">"এপৰ চিহ্ন"</string> <string name="fullscreen_text" msgid="1162316685217676079">"সম্পূৰ্ণ স্ক্ৰীন"</string> <string name="desktop_text" msgid="1077633567027630454">"ডেস্কটপ ম’ড"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"ওপঙা"</string> <string name="select_text" msgid="5139083974039906583">"বাছনি কৰক"</string> <string name="screenshot_text" msgid="1477704010087786671">"স্ক্ৰীনশ্বট"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"ব্ৰাউজাৰত খোলক"</string> + <string name="new_window_text" msgid="6318648868380652280">"নতুন ৱিণ্ড’"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"ৱিণ্ড’ পৰিচালনা কৰক"</string> <string name="close_text" msgid="4986518933445178928">"বন্ধ কৰক"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"মেনু বন্ধ কৰক"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"মেনু খোলক"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"মেনু খোলক"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"স্ক্ৰীন মেক্সিমাইজ কৰক"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"স্ক্ৰীন স্নেপ কৰক"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"এই এপ্টোৰ আকাৰ সলনি কৰিব নোৱাৰি"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"মেক্সিমাইজ কৰক"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"বাওঁফাললৈ স্নেপ কৰক"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"সোঁফাললৈ স্নেপ কৰক"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml index 0d233d72ea13..75ba1ff0a757 100644 --- a/libs/WindowManager/Shell/res/values-az/strings.xml +++ b/libs/WindowManager/Shell/res/values-az/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Yuxarıya sağa köçürün"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Aşağıya sola köçürün"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Aşağıya sağa köçürün"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"menyunu genişləndirin"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"menyunu yığcamlaşdırın"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Sola köçürün"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Sağa köçürün"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"genişləndirin: <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"yığcamlaşdırın: <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ayarları"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Yumrucuğu ləğv edin"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Tam ekrana keçin"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Söhbəti yumrucuqda göstərmə"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Yumrucuqlardan istifadə edərək söhbət edin"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Yeni söhbətlər üzən nişanlar və ya yumrucuqlar kimi görünür. Yumrucuğu açmaq üçün toxunun. Hərəkət etdirmək üçün sürüşdürün."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Kiçildin"</string> <string name="close_button_text" msgid="2913281996024033299">"Bağlayın"</string> <string name="back_button_text" msgid="1469718707134137085">"Geriyə"</string> - <string name="handle_text" msgid="1766582106752184456">"Hər kəsə açıq istifadəçi adı"</string> + <string name="handle_text" msgid="4419667835599523257">"Tətbiq ləqəbi"</string> <string name="app_icon_text" msgid="2823268023931811747">"Tətbiq ikonası"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Tam Ekran"</string> <string name="desktop_text" msgid="1077633567027630454">"Masaüstü Rejimi"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Üzən pəncərə"</string> <string name="select_text" msgid="5139083974039906583">"Seçin"</string> <string name="screenshot_text" msgid="1477704010087786671">"Skrinşot"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Brauzerdə açın"</string> + <string name="new_window_text" msgid="6318648868380652280">"Yeni pəncərə"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Pəncərələri idarə edin"</string> <string name="close_text" msgid="4986518933445178928">"Bağlayın"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menyunu bağlayın"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Menyunu açın"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Menyunu açın"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ekranı maksimum böyüdün"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ekranı çəkin"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Bu tətbiqin ölçüsünü dəyişmək olmur"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Böyüdün"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Sola tərəf çəkin"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Sağa tərəf çəkin"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml index acb8b4f53689..1aaca37dba36 100644 --- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml +++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Premesti gore desno"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Premesti dole levo"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Premesti dole desno"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"proširi meni"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"skupi meni"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Pomerite nalevo"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Pomerite nadesno"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"proširite oblačić <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"skupite oblačić <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Podešavanja za <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Odbaci oblačić"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Prebaci na ceo ekran"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ne koristi oblačiće za konverzaciju"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Ćaskajte u oblačićima"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nove konverzacije se prikazuju kao plutajuće ikone ili oblačići. Dodirnite da biste otvorili oblačić. Prevucite da biste ga premestili."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Umanjite"</string> <string name="close_button_text" msgid="2913281996024033299">"Zatvorite"</string> <string name="back_button_text" msgid="1469718707134137085">"Nazad"</string> - <string name="handle_text" msgid="1766582106752184456">"Identifikator"</string> + <string name="handle_text" msgid="4419667835599523257">"Identifikator aplikacije"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ikona aplikacije"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Preko celog ekrana"</string> <string name="desktop_text" msgid="1077633567027630454">"Režim za računare"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Plutajuće"</string> <string name="select_text" msgid="5139083974039906583">"Izaberite"</string> <string name="screenshot_text" msgid="1477704010087786671">"Snimak ekrana"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Otvorite u pregledaču"</string> + <string name="new_window_text" msgid="6318648868380652280">"Novi prozor"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Upravljajte prozorima"</string> <string name="close_text" msgid="4986518933445178928">"Zatvorite"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zatvorite meni"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Otvorite meni"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Otvorite meni"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Povećaj ekran"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Uklopi ekran"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Veličina ove aplikacije ne može da se promeni"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Uvećajte"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Prikačite levo"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Prikačite desno"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml index 234441639fb7..905707a80629 100644 --- a/libs/WindowManager/Shell/res/values-be/strings.xml +++ b/libs/WindowManager/Shell/res/values-be/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Перамясціце правей і вышэй"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Перамясціць лявей і ніжэй"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Перамясціць правей і ніжэй"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"разгарнуць меню"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"згарнуць меню"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Перамясціць улева"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Перамясціць управа"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>: разгарнуць"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>: згарнуць"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Налады \"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>\""</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Адхіліць апавяшчэнне"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Адкрыць у поўнаэкранным рэжыме"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Не паказваць размову ў выглядзе ўсплывальных апавяшчэнняў"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Усплывальныя чаты"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Новыя размовы будуць паказвацца як рухомыя значкі ці ўсплывальныя чаты. Націсніце, каб адкрыць усплывальны чат. Перацягніце яго, каб перамясціць."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Згарнуць"</string> <string name="close_button_text" msgid="2913281996024033299">"Закрыць"</string> <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> - <string name="handle_text" msgid="1766582106752184456">"Маркер"</string> + <string name="handle_text" msgid="4419667835599523257">"Маркер праграмы"</string> <string name="app_icon_text" msgid="2823268023931811747">"Значок праграмы"</string> <string name="fullscreen_text" msgid="1162316685217676079">"На ўвесь экран"</string> <string name="desktop_text" msgid="1077633567027630454">"Рэжым працоўнага стала"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Зрабіць рухомым акном"</string> <string name="select_text" msgid="5139083974039906583">"Выбраць"</string> <string name="screenshot_text" msgid="1477704010087786671">"Здымак экрана"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Адкрыць у браўзеры"</string> + <string name="new_window_text" msgid="6318648868380652280">"Новае акно"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Кіраваць вокнамі"</string> <string name="close_text" msgid="4986518933445178928">"Закрыць"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Закрыць меню"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Адкрыць меню"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Адкрыць меню"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Разгарнуць на ўвесь экран"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Размясціць на палавіне экрана"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Немагчыма змяніць памер праграмы"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Разгарнуць"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Размясціць злева"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Размясціць справа"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml index 2cb02fe5fdc2..f5e94da56055 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Преместване горе вдясно"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Преместване долу вляво"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Преместване долу вдясно"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"разгъване на менюто"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"свиване на менюто"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Преместване наляво"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Преместване надясно"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"разгъване на <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"свиване на <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Настройки за <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Отхвърляне на балончетата"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Преместване на цял екран"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Без балончета за разговора"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Чат с балончета"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Новите разговори се показват като плаващи икони, или балончета. Докоснете балонче, за да го отворите, или го плъзнете, за да го преместите."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Намаляване"</string> <string name="close_button_text" msgid="2913281996024033299">"Затваряне"</string> <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> - <string name="handle_text" msgid="1766582106752184456">"Манипулатор"</string> + <string name="handle_text" msgid="4419667835599523257">"Манипулатор за приложението"</string> <string name="app_icon_text" msgid="2823268023931811747">"Икона на приложението"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Цял екран"</string> <string name="desktop_text" msgid="1077633567027630454">"Режим за настолни компютри"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Плаващо"</string> <string name="select_text" msgid="5139083974039906583">"Избиране"</string> <string name="screenshot_text" msgid="1477704010087786671">"Екранна снимка"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Отваряне в браузър"</string> + <string name="new_window_text" msgid="6318648868380652280">"Нов прозорец"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Управление на прозорците"</string> <string name="close_text" msgid="4986518933445178928">"Затваряне"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Затваряне на менюто"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Отваряне на менюто"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Отваряне на менюто"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Увеличаване на екрана"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Прилепване на екрана"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Това приложение не може да бъде преоразмерено"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Увеличаване"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Прилепване наляво"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Прилепване надясно"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bn/strings.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml index c0aa94b5a576..5d6374403662 100644 --- a/libs/WindowManager/Shell/res/values-bn/strings.xml +++ b/libs/WindowManager/Shell/res/values-bn/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"উপরে ডানদিকে সরান"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"নিচে বাঁদিকে সরান"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"নিচে ডান দিকে সরান"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"মেনু বড় করে দেখুন"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"মেনু আড়াল করুন"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"বাঁদিকে সরান"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"ডানদিকে সরান"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> বড় করুন"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> আড়াল করুন"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> সেটিংস"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"বাবল খারিজ করুন"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"ফুল-স্ক্রিন ব্যবহার করুন"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"কথোপকথন বাবল হিসেবে দেখাবে না"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"বাবল ব্যবহার করে চ্যাট করুন"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"নতুন কথোপকথন ভেসে থাকা আইকন বা বাবল হিসেবে দেখানো হয়। বাবল খুলতে ট্যাপ করুন। সেটি সরাতে ধরে টেনে আনুন।"</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"ছোট করুন"</string> <string name="close_button_text" msgid="2913281996024033299">"বন্ধ করুন"</string> <string name="back_button_text" msgid="1469718707134137085">"ফিরে যান"</string> - <string name="handle_text" msgid="1766582106752184456">"হাতল"</string> + <string name="handle_text" msgid="4419667835599523257">"অ্যাপের হ্যান্ডেল"</string> <string name="app_icon_text" msgid="2823268023931811747">"অ্যাপ আইকন"</string> <string name="fullscreen_text" msgid="1162316685217676079">"ফুলস্ক্রিন"</string> <string name="desktop_text" msgid="1077633567027630454">"ডেস্কটপ মোড"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"ফ্লোট"</string> <string name="select_text" msgid="5139083974039906583">"বেছে নিন"</string> <string name="screenshot_text" msgid="1477704010087786671">"স্ক্রিনশট"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"ব্রাউজারে খুলুন"</string> + <string name="new_window_text" msgid="6318648868380652280">"নতুন উইন্ডো"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"উইন্ডো ম্যানেজ করুন"</string> <string name="close_text" msgid="4986518933445178928">"বন্ধ করুন"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"\'মেনু\' বন্ধ করুন"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"মেনু খুলুন"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"মেনু খুলুন"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"স্ক্রিন বড় করুন"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"স্ক্রিনে অ্যাপ মানানসই হিসেবে ছোট বড় করুন"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"এই অ্যাপ ছোট বড় করা যাবে না"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"বড় করুন"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"বাঁদিকে স্ন্যাপ করুন"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"ডানদিকে স্ন্যাপ করুন"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml index 784e1aa8128c..bac1ecd11207 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Pomjerite gore desno"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Pomjeri dolje lijevo"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Pomjerite dolje desno"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"proširivanje menija"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"sužavanje menija"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Pomicanje ulijevo"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Pomicanje udesno"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"proširivanje oblačića <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"sužavanje oblačića <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Postavke aplikacije <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Odbaci oblačić"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Prikaži preko cijelog ekrana"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nemoj prikazivati razgovor u oblačićima"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatajte koristeći oblačiće"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Novi razgovori se prikazuju kao plutajuće ikone ili oblačići. Dodirnite da otvorite oblačić. Prevucite da ga premjestite."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimiziranje"</string> <string name="close_button_text" msgid="2913281996024033299">"Zatvaranje"</string> <string name="back_button_text" msgid="1469718707134137085">"Nazad"</string> - <string name="handle_text" msgid="1766582106752184456">"Identifikator"</string> + <string name="handle_text" msgid="4419667835599523257">"Ručica aplikacije"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ikona aplikacije"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Cijeli ekran"</string> <string name="desktop_text" msgid="1077633567027630454">"Način rada radne površine"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Lebdeći"</string> <string name="select_text" msgid="5139083974039906583">"Odabir"</string> <string name="screenshot_text" msgid="1477704010087786671">"Snimak ekrana"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Otvaranje u pregledniku"</string> + <string name="new_window_text" msgid="6318648868380652280">"Novi prozor"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Upravljanje prozorima"</string> <string name="close_text" msgid="4986518933445178928">"Zatvaranje"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zatvaranje menija"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Otvaranje menija"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Otvaranje menija"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimiziraj ekran"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snimi ekran"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Nije moguće promijeniti veličinu aplikacije"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimiziranje"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Pomicanje ulijevo"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Pomicanje udesno"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml index 03dca2adc747..69e7d850078f 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mou a dalt a la dreta"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mou a baix a l\'esquerra"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mou a baix a la dreta"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"desplega el menú"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"replega el menú"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Mou cap a l\'esquerra"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Mou cap a la dreta"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"desplega <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"replega <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Configuració de l\'aplicació <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ignora la bombolla"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Mou a pantalla completa"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"No mostris la conversa com a bombolla"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Xateja amb bombolles"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Les converses noves es mostren com a icones flotants o bombolles. Toca per obrir una bombolla. Arrossega-la per moure-la."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimitza"</string> <string name="close_button_text" msgid="2913281996024033299">"Tanca"</string> <string name="back_button_text" msgid="1469718707134137085">"Enrere"</string> - <string name="handle_text" msgid="1766582106752184456">"Ansa"</string> + <string name="handle_text" msgid="4419667835599523257">"Identificador de l\'aplicació"</string> <string name="app_icon_text" msgid="2823268023931811747">"Icona de l\'aplicació"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Pantalla completa"</string> <string name="desktop_text" msgid="1077633567027630454">"Mode d\'escriptori"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Flotant"</string> <string name="select_text" msgid="5139083974039906583">"Selecciona"</string> <string name="screenshot_text" msgid="1477704010087786671">"Captura de pantalla"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Obre al navegador"</string> + <string name="new_window_text" msgid="6318648868380652280">"Finestra nova"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Gestiona les finestres"</string> <string name="close_text" msgid="4986518933445178928">"Tanca"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Tanca el menú"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Obre el menú"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Obre el menú"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximitza la pantalla"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajusta la pantalla"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"No es pot canviar la mida d\'aquesta aplicació"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximitza"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Ajusta a l\'esquerra"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Ajusta a la dreta"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-cs/strings.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml index 6ebdd8c70228..716491020c65 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Přesunout vpravo nahoru"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Přesunout vlevo dolů"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Přesunout vpravo dolů"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"rozbalit nabídku"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"sbalit nabídku"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Přesunout doleva"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Přesunout doprava"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"rozbalit <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"sbalit <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Nastavení <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Zavřít bublinu"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Přejít na celou obrazovku"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nezobrazovat konverzaci v bublinách"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatujte pomocí bublin"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nové konverzace se zobrazují jako plovoucí ikony, neboli bubliny. Klepnutím bublinu otevřete. Přetažením ji posunete."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimalizovat"</string> <string name="close_button_text" msgid="2913281996024033299">"Zavřít"</string> <string name="back_button_text" msgid="1469718707134137085">"Zpět"</string> - <string name="handle_text" msgid="1766582106752184456">"Úchyt"</string> + <string name="handle_text" msgid="4419667835599523257">"Popisovač aplikace"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ikona aplikace"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Celá obrazovka"</string> <string name="desktop_text" msgid="1077633567027630454">"Režim počítače"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Plovoucí"</string> <string name="select_text" msgid="5139083974039906583">"Vybrat"</string> <string name="screenshot_text" msgid="1477704010087786671">"Snímek obrazovky"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Otevřít v prohlížeči"</string> + <string name="new_window_text" msgid="6318648868380652280">"Nové okno"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Spravovat okna"</string> <string name="close_text" msgid="4986518933445178928">"Zavřít"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zavřít nabídku"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Otevřít nabídku"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Otevřít nabídku"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximalizovat obrazovku"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Rozpůlit obrazovku"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Velikost aplikace nelze změnit"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximalizovat"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Přichytit vlevo"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Přichytit vpravo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml index 04c5ff987bd1..1b264c71cfc5 100644 --- a/libs/WindowManager/Shell/res/values-da/strings.xml +++ b/libs/WindowManager/Shell/res/values-da/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Flyt op til højre"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Flyt ned til venstre"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Flyt ned til højre"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"udvid menu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"minimer menu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Flyt til venstre"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Flyt til højre"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"udvid <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"skjul <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Indstillinger for <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Luk boble"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Flyt til fuld skærm"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Vis ikke samtale i boble"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat ved hjælp af bobler"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nye samtaler vises som svævende ikoner eller bobler. Tryk for at åbne boblen. Træk for at flytte den."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimer"</string> <string name="close_button_text" msgid="2913281996024033299">"Luk"</string> <string name="back_button_text" msgid="1469718707134137085">"Tilbage"</string> - <string name="handle_text" msgid="1766582106752184456">"Håndtag"</string> + <string name="handle_text" msgid="4419667835599523257">"Apphåndtag"</string> <string name="app_icon_text" msgid="2823268023931811747">"Appikon"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Fuld skærm"</string> <string name="desktop_text" msgid="1077633567027630454">"Computertilstand"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Svævende"</string> <string name="select_text" msgid="5139083974039906583">"Vælg"</string> <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Åbn i browser"</string> + <string name="new_window_text" msgid="6318648868380652280">"Nyt vindue"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Administrer vinduer"</string> <string name="close_text" msgid="4986518933445178928">"Luk"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Luk menu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Åbn menu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Åbn menu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimér skærm"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Tilpas skærm"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Størrelsen på denne app kan ikke justeres"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimér"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Fastgør til venstre"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Fastgør til højre"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml index 5704d4f776c8..01a0807dc5b0 100644 --- a/libs/WindowManager/Shell/res/values-de/strings.xml +++ b/libs/WindowManager/Shell/res/values-de/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Nach oben rechts verschieben"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Nach unten links verschieben"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Nach unten rechts verschieben"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"Menü maximieren"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"Menü minimieren"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Nach links bewegen"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Nach rechts bewegen"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> maximieren"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> minimieren"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Einstellungen für <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Bubble schließen"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Vollbildmodus"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Unterhaltung nicht als Bubble anzeigen"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Bubbles zum Chatten verwenden"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Neue Unterhaltungen erscheinen als unverankerte Symbole, „Bubbles“ genannt. Wenn du eine Bubble öffnen möchtest, tippe sie an. Wenn du sie verschieben möchtest, zieh an ihr."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimieren"</string> <string name="close_button_text" msgid="2913281996024033299">"Schließen"</string> <string name="back_button_text" msgid="1469718707134137085">"Zurück"</string> - <string name="handle_text" msgid="1766582106752184456">"Ziehpunkt"</string> + <string name="handle_text" msgid="4419667835599523257">"App-Ziehpunkt"</string> <string name="app_icon_text" msgid="2823268023931811747">"App-Symbol"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Vollbild"</string> <string name="desktop_text" msgid="1077633567027630454">"Desktopmodus"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Frei schwebend"</string> <string name="select_text" msgid="5139083974039906583">"Auswählen"</string> <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Im Browser öffnen"</string> + <string name="new_window_text" msgid="6318648868380652280">"Neues Fenster"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Fenster verwalten"</string> <string name="close_text" msgid="4986518933445178928">"Schließen"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menü schließen"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Menü öffnen"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Menü öffnen"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Bildschirm maximieren"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Bildschirm teilen"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Die Größe dieser App kann nicht geändert werden"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximieren"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Links andocken"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Rechts andocken"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml index 0db71fae0b83..c5c47cbff484 100644 --- a/libs/WindowManager/Shell/res/values-el/strings.xml +++ b/libs/WindowManager/Shell/res/values-el/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Μετακίνηση επάνω δεξιά"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Μετακίνηση κάτω αριστερά"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Μετακίνηση κάτω δεξιά"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"ανάπτυξη μενού"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"σύμπτυξη μενού"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Μετακίνηση αριστερά"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Μετακίνηση δεξιά"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"ανάπτυξη <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"σύμπτυξη <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Ρυθμίσεις <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Παράβλ. για συννεφ."</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Μετακίνηση σε πλήρη οθόνη"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Να μην γίνει προβολή της συζήτησης σε συννεφάκια."</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Συζητήστε χρησιμοποιώντας συννεφάκια."</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Οι νέες συζητήσεις εμφανίζονται ως κινούμενα εικονίδια ή συννεφάκια. Πατήστε για να ανοίξετε το συννεφάκι. Σύρετε για να το μετακινήσετε."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Ελαχιστοποίηση"</string> <string name="close_button_text" msgid="2913281996024033299">"Κλείσιμο"</string> <string name="back_button_text" msgid="1469718707134137085">"Πίσω"</string> - <string name="handle_text" msgid="1766582106752184456">"Λαβή"</string> + <string name="handle_text" msgid="4419667835599523257">"Λαβή εφαρμογής"</string> <string name="app_icon_text" msgid="2823268023931811747">"Εικονίδιο εφαρμογής"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Πλήρης οθόνη"</string> <string name="desktop_text" msgid="1077633567027630454">"Λειτουργία επιφάνειας εργασίας"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Κινούμενο"</string> <string name="select_text" msgid="5139083974039906583">"Επιλογή"</string> <string name="screenshot_text" msgid="1477704010087786671">"Στιγμιότυπο οθόνης"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Άνοιγμα σε πρόγραμμα περιήγησης"</string> + <string name="new_window_text" msgid="6318648868380652280">"Νέο παράθυρο"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Διαχείριση παραθύρων"</string> <string name="close_text" msgid="4986518933445178928">"Κλείσιμο"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Κλείσιμο μενού"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Άνοιγμα μενού"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Άνοιγμα μενού"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Μεγιστοποίηση οθόνης"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Προβολή στο μισό της οθόνης"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Δεν είναι δυνατή η αλλαγή μεγέθους αυτής της εφαρμογής"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Μεγιστοποίηση"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Κούμπωμα αριστερά"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Κούμπωμα δεξιά"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml index ec477128ac8e..766852d4b1f0 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Move top right"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Move bottom left"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Move bottom right"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"expand menu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"collapse menu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Move left"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Move right"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"expand <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"collapse <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> settings"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dismiss bubble"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Move to fullscreen"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Don’t bubble conversation"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat using bubbles"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"New conversations appear as floating icons, or bubbles. Tap to open bubble. Drag to move it."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimise"</string> <string name="close_button_text" msgid="2913281996024033299">"Close"</string> <string name="back_button_text" msgid="1469718707134137085">"Back"</string> - <string name="handle_text" msgid="1766582106752184456">"Handle"</string> + <string name="handle_text" msgid="4419667835599523257">"App handle"</string> <string name="app_icon_text" msgid="2823268023931811747">"App icon"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Full screen"</string> <string name="desktop_text" msgid="1077633567027630454">"Desktop mode"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Float"</string> <string name="select_text" msgid="5139083974039906583">"Select"</string> <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Open in browser"</string> + <string name="new_window_text" msgid="6318648868380652280">"New window"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Manage windows"</string> <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Open menu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Open menu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"This app can\'t be resized"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximise"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Snap left"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Snap right"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml index d7e23fd8dfd8..aa3a484079f8 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Move top right"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Move bottom left"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Move bottom right"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"expand menu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"collapse menu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Move left"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Move right"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"expand <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"collapse <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> settings"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dismiss bubble"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Move to fullscreen"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Don’t bubble conversation"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat using bubbles"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"New conversations appear as floating icons, or bubbles. Tap to open bubble. Drag to move it."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimize"</string> <string name="close_button_text" msgid="2913281996024033299">"Close"</string> <string name="back_button_text" msgid="1469718707134137085">"Back"</string> - <string name="handle_text" msgid="1766582106752184456">"Handle"</string> + <string name="handle_text" msgid="4419667835599523257">"App handle"</string> <string name="app_icon_text" msgid="2823268023931811747">"App Icon"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Fullscreen"</string> <string name="desktop_text" msgid="1077633567027630454">"Desktop Mode"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Float"</string> <string name="select_text" msgid="5139083974039906583">"Select"</string> <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Open in browser"</string> + <string name="new_window_text" msgid="6318648868380652280">"New Window"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Manage Windows"</string> <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close Menu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Open Menu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Open Menu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximize Screen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap Screen"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"This app can\'t be resized"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximize"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Snap left"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Snap right"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml index ec477128ac8e..766852d4b1f0 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Move top right"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Move bottom left"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Move bottom right"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"expand menu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"collapse menu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Move left"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Move right"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"expand <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"collapse <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> settings"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dismiss bubble"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Move to fullscreen"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Don’t bubble conversation"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat using bubbles"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"New conversations appear as floating icons, or bubbles. Tap to open bubble. Drag to move it."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimise"</string> <string name="close_button_text" msgid="2913281996024033299">"Close"</string> <string name="back_button_text" msgid="1469718707134137085">"Back"</string> - <string name="handle_text" msgid="1766582106752184456">"Handle"</string> + <string name="handle_text" msgid="4419667835599523257">"App handle"</string> <string name="app_icon_text" msgid="2823268023931811747">"App icon"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Full screen"</string> <string name="desktop_text" msgid="1077633567027630454">"Desktop mode"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Float"</string> <string name="select_text" msgid="5139083974039906583">"Select"</string> <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Open in browser"</string> + <string name="new_window_text" msgid="6318648868380652280">"New window"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Manage windows"</string> <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Open menu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Open menu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"This app can\'t be resized"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximise"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Snap left"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Snap right"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml index ec477128ac8e..766852d4b1f0 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Move top right"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Move bottom left"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Move bottom right"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"expand menu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"collapse menu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Move left"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Move right"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"expand <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"collapse <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> settings"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dismiss bubble"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Move to fullscreen"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Don’t bubble conversation"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat using bubbles"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"New conversations appear as floating icons, or bubbles. Tap to open bubble. Drag to move it."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimise"</string> <string name="close_button_text" msgid="2913281996024033299">"Close"</string> <string name="back_button_text" msgid="1469718707134137085">"Back"</string> - <string name="handle_text" msgid="1766582106752184456">"Handle"</string> + <string name="handle_text" msgid="4419667835599523257">"App handle"</string> <string name="app_icon_text" msgid="2823268023931811747">"App icon"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Full screen"</string> <string name="desktop_text" msgid="1077633567027630454">"Desktop mode"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Float"</string> <string name="select_text" msgid="5139083974039906583">"Select"</string> <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Open in browser"</string> + <string name="new_window_text" msgid="6318648868380652280">"New window"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Manage windows"</string> <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Open menu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Open menu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"This app can\'t be resized"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximise"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Snap left"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Snap right"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml index 1da8c275ce54..bda5132156cd 100644 --- a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Move top right"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Move bottom left"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Move bottom right"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"expand menu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"collapse menu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Move left"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Move right"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"expand <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"collapse <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> settings"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dismiss bubble"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Move to fullscreen"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Don’t bubble conversation"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat using bubbles"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"New conversations appear as floating icons, or bubbles. Tap to open bubble. Drag to move it."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimize"</string> <string name="close_button_text" msgid="2913281996024033299">"Close"</string> <string name="back_button_text" msgid="1469718707134137085">"Back"</string> - <string name="handle_text" msgid="1766582106752184456">"Handle"</string> + <string name="handle_text" msgid="4419667835599523257">"App handle"</string> <string name="app_icon_text" msgid="2823268023931811747">"App Icon"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Fullscreen"</string> <string name="desktop_text" msgid="1077633567027630454">"Desktop Mode"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Float"</string> <string name="select_text" msgid="5139083974039906583">"Select"</string> <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Open in browser"</string> + <string name="new_window_text" msgid="6318648868380652280">"New Window"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Manage Windows"</string> <string name="close_text" msgid="4986518933445178928">"Close"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Close Menu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Open Menu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Open Menu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximize Screen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap Screen"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"This app can\'t be resized"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximize"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Snap left"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Snap right"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml index 99a89edc19e9..4dc39919f224 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Ubicar arriba a la derecha"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Ubicar abajo a la izquierda"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Ubicar abajo a la derecha"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"expandir menú"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"contraer menú"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Mover hacia la izquierda"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Mover hacia la derecha"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"expandir <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"contraer <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Configuración de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Descartar burbuja"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Mover a pantalla completa"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"No mostrar la conversación en burbuja"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat con burbujas"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Las conversaciones nuevas aparecen como elementos flotantes o burbujas. Presiona para abrir la burbuja. Arrástrala para moverla."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string> <string name="close_button_text" msgid="2913281996024033299">"Cerrar"</string> <string name="back_button_text" msgid="1469718707134137085">"Atrás"</string> - <string name="handle_text" msgid="1766582106752184456">"Controlador"</string> + <string name="handle_text" msgid="4419667835599523257">"Controlador de la app"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ícono de la app"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Pantalla completa"</string> <string name="desktop_text" msgid="1077633567027630454">"Modo de escritorio"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Flotante"</string> <string name="select_text" msgid="5139083974039906583">"Seleccionar"</string> <string name="screenshot_text" msgid="1477704010087786671">"Captura de pantalla"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Abrir en el navegador"</string> + <string name="new_window_text" msgid="6318648868380652280">"Nueva ventana"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Administrar ventanas"</string> <string name="close_text" msgid="4986518933445178928">"Cerrar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Cerrar menú"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Abrir el menú"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Abrir el menú"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar pantalla"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar pantalla"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"No se puede cambiar el tamaño de esta app"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizar"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Ajustar a la izquierda"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Ajustar a la derecha"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml index 53a490e00e9f..ad3de78a8d94 100644 --- a/libs/WindowManager/Shell/res/values-es/strings.xml +++ b/libs/WindowManager/Shell/res/values-es/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover arriba a la derecha"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover abajo a la izquierda."</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover abajo a la derecha"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"mostrar menú"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"ocultar menú"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Mover hacia la izquierda"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Mover hacia la derecha"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"desplegar <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"contraer <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Ajustes de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Cerrar burbuja"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Cambiar a pantalla completa"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"No mostrar conversación en burbuja"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatea con burbujas"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Las conversaciones nuevas aparecen como iconos flotantes llamados \"burbujas\". Toca una burbuja para abrirla. Arrástrala para moverla."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string> <string name="close_button_text" msgid="2913281996024033299">"Cerrar"</string> <string name="back_button_text" msgid="1469718707134137085">"Atrás"</string> - <string name="handle_text" msgid="1766582106752184456">"Controlador"</string> + <string name="handle_text" msgid="4419667835599523257">"Controlador de la aplicación"</string> <string name="app_icon_text" msgid="2823268023931811747">"Icono de la aplicación"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Pantalla completa"</string> <string name="desktop_text" msgid="1077633567027630454">"Modo Escritorio"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Flotante"</string> <string name="select_text" msgid="5139083974039906583">"Seleccionar"</string> <string name="screenshot_text" msgid="1477704010087786671">"Captura de pantalla"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Abrir en el navegador"</string> + <string name="new_window_text" msgid="6318648868380652280">"Ventana nueva"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Gestionar ventanas"</string> <string name="close_text" msgid="4986518933445178928">"Cerrar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Cerrar menú"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Abrir menú"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Abrir menú"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar pantalla"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar pantalla"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"No se puede cambiar el tamaño de esta aplicación"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizar"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Acoplar a la izquierda"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Acoplar a la derecha"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml index 19d453630801..deeb80b0504c 100644 --- a/libs/WindowManager/Shell/res/values-et/strings.xml +++ b/libs/WindowManager/Shell/res/values-et/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Teisalda üles paremale"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Teisalda alla vasakule"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Teisalda alla paremale"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"menüü laiendamine"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"menüü ahendamine"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Liiguta vasakule"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Liiguta paremale"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"laienda <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"ahenda <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Rakenduse <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> seaded"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Sule mull"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Lülitu täisekraanile"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ära kuva vestlust mullina"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Vestelge mullide abil"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Uued vestlused kuvatakse hõljuvate ikoonidena ehk mullidena. Puudutage mulli avamiseks. Lohistage mulli, et seda liigutada."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimeeri"</string> <string name="close_button_text" msgid="2913281996024033299">"Sule"</string> <string name="back_button_text" msgid="1469718707134137085">"Tagasi"</string> - <string name="handle_text" msgid="1766582106752184456">"Käepide"</string> + <string name="handle_text" msgid="4419667835599523257">"Rakenduse element"</string> <string name="app_icon_text" msgid="2823268023931811747">"Rakenduse ikoon"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Täisekraan"</string> <string name="desktop_text" msgid="1077633567027630454">"Lauaarvuti režiim"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Hõljuv"</string> <string name="select_text" msgid="5139083974039906583">"Vali"</string> <string name="screenshot_text" msgid="1477704010087786671">"Ekraanipilt"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Avamine brauseris"</string> + <string name="new_window_text" msgid="6318648868380652280">"Uus aken"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Akende haldamine"</string> <string name="close_text" msgid="4986518933445178928">"Sule"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Sule menüü"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Ava menüü"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Ava menüü"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Kuva täisekraanil"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Kuva poolel ekraanil"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Selle rakenduse aknasuurust ei saa muuta"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimeeri"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Tõmmake vasakule"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Tõmmake paremale"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml index 8d676728b9e6..673947030fe6 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Eraman goialdera, eskuinetara"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Eraman behealdera, ezkerretara"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Eraman behealdera, eskuinetara"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"zabaldu menua"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"tolestu menua"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Eraman ezkerrera"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Eraman eskuinera"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"zabaldu <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"tolestu <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> aplikazioaren ezarpenak"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Baztertu burbuila"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Joan pantaila osora"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ez erakutsi elkarrizketak burbuila gisa"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Txateatu burbuilen bidez"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Elkarrizketa berriak ikono gainerakor edo burbuila gisa agertzen dira. Sakatu burbuila irekitzeko. Arrasta ezazu mugitzeko."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimizatu"</string> <string name="close_button_text" msgid="2913281996024033299">"Itxi"</string> <string name="back_button_text" msgid="1469718707134137085">"Atzera"</string> - <string name="handle_text" msgid="1766582106752184456">"Kontu-izena"</string> + <string name="handle_text" msgid="4419667835599523257">"Aplikazioaren kontrol-puntua"</string> <string name="app_icon_text" msgid="2823268023931811747">"Aplikazioaren ikonoa"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Pantaila osoa"</string> <string name="desktop_text" msgid="1077633567027630454">"Ordenagailuetarako modua"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Leiho gainerakorra"</string> <string name="select_text" msgid="5139083974039906583">"Hautatu"</string> <string name="screenshot_text" msgid="1477704010087786671">"Pantaila-argazkia"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Ireki arakatzailean"</string> + <string name="new_window_text" msgid="6318648868380652280">"Leiho berria"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Kudeatu leihoak"</string> <string name="close_text" msgid="4986518933445178928">"Itxi"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Itxi menua"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Ireki menua"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Ireki menua"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Handitu pantaila"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Zatitu pantaila"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Ezin zaio aldatu tamaina aplikazio honi"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizatu"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Ezarri ezkerrean"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Ezarri eskuinean"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml index ccbd65f82012..896456d328e2 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"انتقال به بالا سمت چپ"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"انتقال به پایین سمت راست"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"انتقال به پایین سمت چپ"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"ازهم بازکردن منو"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"جمع کردن منو"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"انتقال بهچپ"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"انتقال بهراست"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"ازهم باز کردن <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"جمع کردن <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"تنظیمات <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"رد کردن حبابک"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"رفتن به حالت تمامصفحه"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"مکالمه در حباب نشان داده نشود"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"گپ بااستفاده از حبابکها"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"مکالمههای جدید بهصورت نمادهای شناور یا حبابکها نشان داده میشوند. برای باز کردن حبابکها تکضرب بزنید. برای جابهجایی، آن را بکشید."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"کوچک کردن"</string> <string name="close_button_text" msgid="2913281996024033299">"بستن"</string> <string name="back_button_text" msgid="1469718707134137085">"برگشتن"</string> - <string name="handle_text" msgid="1766582106752184456">"دستگیره"</string> + <string name="handle_text" msgid="4419667835599523257">"دستگیره برنامه"</string> <string name="app_icon_text" msgid="2823268023931811747">"نماد برنامه"</string> <string name="fullscreen_text" msgid="1162316685217676079">"تمامصفحه"</string> <string name="desktop_text" msgid="1077633567027630454">"حالت رایانه"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"شناور"</string> <string name="select_text" msgid="5139083974039906583">"انتخاب"</string> <string name="screenshot_text" msgid="1477704010087786671">"نماگرفت"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"باز کردن در مرورگر"</string> + <string name="new_window_text" msgid="6318648868380652280">"پنجره جدید"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"مدیریت کردن پنجرهها"</string> <string name="close_text" msgid="4986518933445178928">"بستن"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"بستن منو"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"باز کردن منو"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"باز کردن منو"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"بزرگ کردن صفحه"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"بزرگ کردن صفحه"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"اندازه این برنامه را نمیتوان تغییر داد"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"بزرگ کردن"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"کشیدن بهچپ"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"کشیدن بهراست"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml index 92c93fde1ba7..7d343d6edb9d 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Siirrä oikeaan yläreunaan"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Siirrä vasempaan alareunaan"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Siirrä oikeaan alareunaan"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"laajenna valikko"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"tiivistä valikko"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Siirrä vasemmalle"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Siirrä oikealle"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"laajenna <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"tiivistä <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>: asetukset"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ohita kupla"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Siirrä koko näytölle"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Älä näytä kuplia keskusteluista"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chattaile kuplien avulla"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Uudet keskustelut näkyvät kelluvina kuvakkeina tai kuplina. Avaa kupla napauttamalla. Siirrä sitä vetämällä."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Pienennä"</string> <string name="close_button_text" msgid="2913281996024033299">"Sulje"</string> <string name="back_button_text" msgid="1469718707134137085">"Takaisin"</string> - <string name="handle_text" msgid="1766582106752184456">"Kahva"</string> + <string name="handle_text" msgid="4419667835599523257">"Sovelluksen tunnus"</string> <string name="app_icon_text" msgid="2823268023931811747">"Sovelluskuvake"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Koko näyttö"</string> <string name="desktop_text" msgid="1077633567027630454">"Työpöytätila"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Kelluva ikkuna"</string> <string name="select_text" msgid="5139083974039906583">"Valitse"</string> <string name="screenshot_text" msgid="1477704010087786671">"Kuvakaappaus"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Avaa selaimessa"</string> + <string name="new_window_text" msgid="6318648868380652280">"Uusi ikkuna"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Hallinnoi ikkunoita"</string> <string name="close_text" msgid="4986518933445178928">"Sulje"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Sulje valikko"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Avaa valikko"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Avaa valikko"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Suurenna näyttö"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Jaa näyttö"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Tämän sovellusikkunan kokoa ei voi muuttaa"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Suurenna"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Siirrä vasemmalle"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Siirrä oikealle"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml index 5cc980650e84..a36d5ce458ab 100644 --- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Déplacer en haut à droite"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Déplacer en bas à gauche"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Déplacer en bas à droite"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"développer le menu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"réduire le menu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Déplacer vers la gauche"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Déplacer vers la droite"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"développer <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"réduire <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Paramètres <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ignorer la bulle"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Passez en plein écran"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ne pas afficher la conversation dans une bulle"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Clavarder en utilisant des bulles"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Les nouvelles conversations s\'affichent sous forme d\'icônes flottantes (de bulles). Touchez une bulle pour l\'ouvrir. Faites-la glisser pour la déplacer."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Réduire"</string> <string name="close_button_text" msgid="2913281996024033299">"Fermer"</string> <string name="back_button_text" msgid="1469718707134137085">"Retour"</string> - <string name="handle_text" msgid="1766582106752184456">"Identifiant"</string> + <string name="handle_text" msgid="4419667835599523257">"Poignée de l\'appli"</string> <string name="app_icon_text" msgid="2823268023931811747">"Icône de l\'appli"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Plein écran"</string> <string name="desktop_text" msgid="1077633567027630454">"Mode Bureau"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Flottant"</string> <string name="select_text" msgid="5139083974039906583">"Sélectionner"</string> <string name="screenshot_text" msgid="1477704010087786671">"Capture d\'écran"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Ouvrir dans le navigateur"</string> + <string name="new_window_text" msgid="6318648868380652280">"Nouvelle fenêtre"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Gérer les fenêtres"</string> <string name="close_text" msgid="4986518933445178928">"Fermer"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fermer le menu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Ouvrir le menu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Ouvrir le menu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Agrandir l\'écran"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Aligner l\'écran"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Impossible de redimensionner cette appli"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Agrandir"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Épingler à gauche"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Épingler à droite"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml index 727a38ea5036..8f5f3de449b4 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Déplacer en haut à droite"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Déplacer en bas à gauche"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Déplacer en bas à droite"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"développer le menu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"réduire le menu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Déplacer vers la gauche"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Déplacer vers la droite"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"Développer <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"Réduire <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Paramètres <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Fermer la bulle"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Passer en plein écran"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ne pas afficher la conversation dans une bulle"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatter en utilisant des bulles"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Les nouvelles conversations s\'affichent sous forme d\'icônes flottantes ou de bulles. Appuyez sur la bulle pour l\'ouvrir. Faites-la glisser pour la déplacer."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Réduire"</string> <string name="close_button_text" msgid="2913281996024033299">"Fermer"</string> <string name="back_button_text" msgid="1469718707134137085">"Retour"</string> - <string name="handle_text" msgid="1766582106752184456">"Poignée"</string> + <string name="handle_text" msgid="4419667835599523257">"Poignée de l\'appli"</string> <string name="app_icon_text" msgid="2823268023931811747">"Icône d\'application"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Plein écran"</string> <string name="desktop_text" msgid="1077633567027630454">"Mode ordinateur"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Flottante"</string> <string name="select_text" msgid="5139083974039906583">"Sélectionner"</string> <string name="screenshot_text" msgid="1477704010087786671">"Capture d\'écran"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Ouvrir dans un navigateur"</string> + <string name="new_window_text" msgid="6318648868380652280">"Nouvelle fenêtre"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Gérer les fenêtres"</string> <string name="close_text" msgid="4986518933445178928">"Fermer"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fermer le menu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Ouvrir le menu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Ouvrir le menu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Mettre en plein écran"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fractionner l\'écran"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Impossible de redimensionner cette appli"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Agrandir"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Ancrer à gauche"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Ancrer à droite"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml index 81a247d643ae..4175ff183d80 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover arriba á dereita"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover abaixo á esquerda"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover abaixo á dereita"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"despregar o menú"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"contraer o menú"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Mover cara á esquerda"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Mover cara á dereita"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"despregar <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"contraer <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Configuración de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ignorar burbulla"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Cambiar á pantalla completa"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Non mostrar a conversa como burbulla"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatear usando burbullas"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"As conversas novas aparecen como iconas flotantes ou burbullas. Toca para abrir a burbulla e arrastra para movela."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string> <string name="close_button_text" msgid="2913281996024033299">"Pechar"</string> <string name="back_button_text" msgid="1469718707134137085">"Atrás"</string> - <string name="handle_text" msgid="1766582106752184456">"Controlador"</string> + <string name="handle_text" msgid="4419667835599523257">"Controlador da aplicación"</string> <string name="app_icon_text" msgid="2823268023931811747">"Icona de aplicación"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Pantalla completa"</string> <string name="desktop_text" msgid="1077633567027630454">"Modo de escritorio"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Flotante"</string> <string name="select_text" msgid="5139083974039906583">"Seleccionar"</string> <string name="screenshot_text" msgid="1477704010087786671">"Captura de pantalla"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Abrir no navegador"</string> + <string name="new_window_text" msgid="6318648868380652280">"Ventá nova"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Xestionar as ventás"</string> <string name="close_text" msgid="4986518933445178928">"Pechar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Pechar o menú"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Abrir menú"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Abrir o menú"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar pantalla"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Encaixar pantalla"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Non se pode cambiar o tamaño desta aplicación"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizar"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Axustar á esquerda"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Axustar á dereita"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-gu/strings.xml b/libs/WindowManager/Shell/res/values-gu/strings.xml index ac5c91ce3a6d..3239f5aedab8 100644 --- a/libs/WindowManager/Shell/res/values-gu/strings.xml +++ b/libs/WindowManager/Shell/res/values-gu/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ઉપર જમણે ખસેડો"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"નીચે ડાબે ખસેડો"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"નીચે જમણે ખસેડો"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"મેનૂ મોટું કરો"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"મેનૂ નાનું કરો"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"ડાબે ખસેડો"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"જમણે ખસેડો"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> મોટું કરો"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> નાનું કરો"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> સેટિંગ"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"બબલને છોડી દો"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"પૂર્ણસ્ક્રીન પર ખસો"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"વાતચીતને બબલ કરશો નહીં"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"બબલનો ઉપયોગ કરીને ચૅટ કરો"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"નવી વાતચીત ફ્લોટિંગ આઇકન અથવા બબલ જેવી દેખાશે. બબલને ખોલવા માટે ટૅપ કરો. તેને ખસેડવા માટે ખેંચો."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"નાનું કરો"</string> <string name="close_button_text" msgid="2913281996024033299">"બંધ કરો"</string> <string name="back_button_text" msgid="1469718707134137085">"પાછળ"</string> - <string name="handle_text" msgid="1766582106752184456">"હૅન્ડલ"</string> + <string name="handle_text" msgid="4419667835599523257">"ઍપનું હૅન્ડલ"</string> <string name="app_icon_text" msgid="2823268023931811747">"ઍપનું આઇકન"</string> <string name="fullscreen_text" msgid="1162316685217676079">"પૂર્ણસ્ક્રીન"</string> <string name="desktop_text" msgid="1077633567027630454">"ડેસ્કટૉપ મોડ"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"ફ્લોટિંગ વિન્ડો"</string> <string name="select_text" msgid="5139083974039906583">"પસંદ કરો"</string> <string name="screenshot_text" msgid="1477704010087786671">"સ્ક્રીનશૉટ"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"બ્રાઉઝરમાં ખોલો"</string> + <string name="new_window_text" msgid="6318648868380652280">"નવી વિન્ડો"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"વિન્ડો મેનેજ કરો"</string> <string name="close_text" msgid="4986518933445178928">"બંધ કરો"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"મેનૂ બંધ કરો"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"મેનૂ ખોલો"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"મેનૂ ખોલો"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"સ્ક્રીન કરો મોટી કરો"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"સ્ક્રીન સ્નૅપ કરો"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"આ ઍપના કદમાં વધઘટ કરી શકાતો નથી"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"મોટું કરો"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ડાબે સ્નૅપ કરો"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"જમણે સ્નૅપ કરો"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml index c4b7bc33d690..7030aacf0bc0 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"सबसे ऊपर दाईं ओर ले जाएं"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"बाईं ओर सबसे नीचे ले जाएं"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"सबसे नीचे दाईं ओर ले जाएं"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"मेन्यू बड़ा करें"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"मेन्यू छोटा करें"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"बाईं ओर ले जाएं"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"दाईं ओर ले जाएं"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> को बड़ा करें"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> को छोटा करें"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> की सेटिंग"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"बबल खारिज करें"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"फ़ुलस्क्रीन पर मूव करें"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"बातचीत को बबल न करें"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"बबल्स का इस्तेमाल करके चैट करें"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"नई बातचीत फ़्लोटिंग आइकॉन या बबल्स की तरह दिखेंगी. बबल को खोलने के लिए टैप करें. इसे एक जगह से दूसरी जगह ले जाने के लिए खींचें और छोड़ें."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"विंडो छोटी करें"</string> <string name="close_button_text" msgid="2913281996024033299">"बंद करें"</string> <string name="back_button_text" msgid="1469718707134137085">"वापस जाएं"</string> - <string name="handle_text" msgid="1766582106752184456">"हैंडल"</string> + <string name="handle_text" msgid="4419667835599523257">"ऐप्लिकेशन का हैंडल"</string> <string name="app_icon_text" msgid="2823268023931811747">"ऐप्लिकेशन आइकॉन"</string> <string name="fullscreen_text" msgid="1162316685217676079">"फ़ुलस्क्रीन"</string> <string name="desktop_text" msgid="1077633567027630454">"डेस्कटॉप मोड"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"फ़्लोट"</string> <string name="select_text" msgid="5139083974039906583">"चुनें"</string> <string name="screenshot_text" msgid="1477704010087786671">"स्क्रीनशॉट"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"ब्राउज़र में खोलें"</string> + <string name="new_window_text" msgid="6318648868380652280">"नई विंडो"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"विंडो मैनेज करें"</string> <string name="close_text" msgid="4986518933445178928">"बंद करें"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"मेन्यू बंद करें"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"मेन्यू खोलें"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"मेन्यू खोलें"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"स्क्रीन को बड़ा करें"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"स्नैप स्क्रीन"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"इस ऐप्लिकेशन का साइज़ नहीं बदला जा सकता"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"बड़ा करें"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"बाईं ओर स्नैप करें"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"दाईं ओर स्नैप करें"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml index 7ef6436ef5fd..4a88ae4cb8c7 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Premjesti u gornji desni kut"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Premjesti u donji lijevi kut"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Premjestite u donji desni kut"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"proširi izbornik"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"sažmi izbornik"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Pomakni ulijevo"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Pomakni udesno"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"proširite oblačić <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"sažmite oblačić <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Postavke za <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Odbaci oblačić"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Prebaci na cijeli zaslon"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Zaustavi razgovor u oblačićima"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Oblačići u chatu"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Novi razgovori pojavljuju se kao pomične ikone ili oblačići. Dodirnite za otvaranje oblačića. Povucite da biste ga premjestili."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimiziraj"</string> <string name="close_button_text" msgid="2913281996024033299">"Zatvori"</string> <string name="back_button_text" msgid="1469718707134137085">"Natrag"</string> - <string name="handle_text" msgid="1766582106752184456">"Pokazivač"</string> + <string name="handle_text" msgid="4419667835599523257">"Pokazivač aplikacije"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ikona aplikacije"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Puni zaslon"</string> <string name="desktop_text" msgid="1077633567027630454">"Stolni način rada"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Plutajući"</string> <string name="select_text" msgid="5139083974039906583">"Odaberite"</string> <string name="screenshot_text" msgid="1477704010087786671">"Snimka zaslona"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Otvori u pregledniku"</string> + <string name="new_window_text" msgid="6318648868380652280">"Novi prozor"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Upravljanje prozorima"</string> <string name="close_text" msgid="4986518933445178928">"Zatvorite"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zatvorite izbornik"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Otvaranje izbornika"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Otvaranje izbornika"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimalno povećaj zaslon"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Izradi snimku zaslona"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Nije moguće promijeniti veličinu aplikacije"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimiziraj"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Poravnaj lijevo"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Poravnaj desno"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hu/strings.xml b/libs/WindowManager/Shell/res/values-hu/strings.xml index 4f448272bedc..fb44cd855a7b 100644 --- a/libs/WindowManager/Shell/res/values-hu/strings.xml +++ b/libs/WindowManager/Shell/res/values-hu/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Áthelyezés fel és jobbra"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Áthelyezés le és balra"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Áthelyezés le és jobbra"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"menü kibontása"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"menü összecsukása"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Mozgatás balra"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Mozgatás jobbra"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> kibontása"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> összecsukása"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> beállításai"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Buborék elvetése"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Áthelyezés teljes képernyőre"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ne jelenjen meg a beszélgetés buborékban"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Buborékokat használó csevegés"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Az új beszélgetések lebegő ikonként, vagyis buborékként jelennek meg. A buborék megnyitásához koppintson rá. Áthelyezéshez húzza a kívánt helyre."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Kis méret"</string> <string name="close_button_text" msgid="2913281996024033299">"Bezárás"</string> <string name="back_button_text" msgid="1469718707134137085">"Vissza"</string> - <string name="handle_text" msgid="1766582106752184456">"Fogópont"</string> + <string name="handle_text" msgid="4419667835599523257">"App fogópontja"</string> <string name="app_icon_text" msgid="2823268023931811747">"Alkalmazásikon"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Teljes képernyő"</string> <string name="desktop_text" msgid="1077633567027630454">"Asztali üzemmód"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Lebegő"</string> <string name="select_text" msgid="5139083974039906583">"Kiválasztás"</string> <string name="screenshot_text" msgid="1477704010087786671">"Képernyőkép"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Megnyitás böngészőben"</string> + <string name="new_window_text" msgid="6318648868380652280">"Új ablak"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Ablakok kezelése"</string> <string name="close_text" msgid="4986518933445178928">"Bezárás"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menü bezárása"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Menü megnyitása"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Menü megnyitása"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Képernyő méretének maximalizálása"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Igazodás a képernyő adott részéhez"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Ezt az alkalmazást nem lehet átméretezni"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Teljes méret"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Balra igazítás"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Jobbra igazítás"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml index 3a5440e87f85..8dc5599e7c19 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Տեղափոխել վերև՝ աջ"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Տեղափոխել ներքև՝ ձախ"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Տեղափոխել ներքև՝ աջ"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"ծավալել ընտրացանկը"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"ծալել ընտրացանկը"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Տեղափոխել ձախ"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Տեղափոխել աջ"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>. ծավալել"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>. ծալել"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> – կարգավորումներ"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Փակել ամպիկը"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Տեղափոխել լիաէկրան ռեժիմ"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Զրույցը չցուցադրել ամպիկի տեսքով"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Զրույցի ամպիկներ"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Նոր զրույցները կհայտնվեն լողացող պատկերակների կամ ամպիկների տեսքով։ Հպեք՝ ամպիկը բացելու համար։ Քաշեք՝ այն տեղափոխելու համար։"</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Ծալել"</string> <string name="close_button_text" msgid="2913281996024033299">"Փակել"</string> <string name="back_button_text" msgid="1469718707134137085">"Հետ"</string> - <string name="handle_text" msgid="1766582106752184456">"Նշիչ"</string> + <string name="handle_text" msgid="4419667835599523257">"Հավելվածի կեղծանուն"</string> <string name="app_icon_text" msgid="2823268023931811747">"Հավելվածի պատկերակ"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Լիաէկրան"</string> <string name="desktop_text" msgid="1077633567027630454">"Համակարգչի ռեժիմ"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Լողացող պատուհան"</string> <string name="select_text" msgid="5139083974039906583">"Ընտրել"</string> <string name="screenshot_text" msgid="1477704010087786671">"Սքրինշոթ"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Բացել դիտարկիչում"</string> + <string name="new_window_text" msgid="6318648868380652280">"Նոր պատուհան"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Կառավարել պատուհանները"</string> <string name="close_text" msgid="4986518933445178928">"Փակել"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Փակել ընտրացանկը"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Բացել ընտրացանկը"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Բացել ընտրացանկը"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ծավալել էկրանը"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ծալել էկրանը"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Այս հավելվածի չափը հնարավոր չէ փոխել"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Ծավալել"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Ամրացնել ձախ կողմում"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Ամրացնել աջ կողմում"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml index 7c4a5c36032e..a434243a135e 100644 --- a/libs/WindowManager/Shell/res/values-in/strings.xml +++ b/libs/WindowManager/Shell/res/values-in/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Pindahkan ke kanan atas"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Pindahkan ke kiri bawah"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Pindahkan ke kanan bawah"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"luaskan menu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"ciutkan menu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Pindahkan ke kiri"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Pindahkan ke kanan"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"luaskan <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"ciutkan <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Setelan <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Tutup balon"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Pindahkan ke layar penuh"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Jangan gunakan percakapan balon"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat dalam tampilan balon"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Percakapan baru muncul sebagai ikon mengambang, atau balon. Ketuk untuk membuka balon. Tarik untuk memindahkannya."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimalkan"</string> <string name="close_button_text" msgid="2913281996024033299">"Tutup"</string> <string name="back_button_text" msgid="1469718707134137085">"Kembali"</string> - <string name="handle_text" msgid="1766582106752184456">"Tuas"</string> + <string name="handle_text" msgid="4419667835599523257">"Penanganan aplikasi"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ikon Aplikasi"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Layar Penuh"</string> <string name="desktop_text" msgid="1077633567027630454">"Mode Desktop"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Mengambang"</string> <string name="select_text" msgid="5139083974039906583">"Pilih"</string> <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Buka di browser"</string> + <string name="new_window_text" msgid="6318648868380652280">"Jendela Baru"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Kelola Jendela"</string> <string name="close_text" msgid="4986518933445178928">"Tutup"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Tutup Menu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Buka Menu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Buka Menu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Perbesar Layar"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Gabungkan Layar"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Ukuran aplikasi ini tidak dapat diubah"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimalkan"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Maksimalkan ke kiri"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Maksimalkan ke kanan"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml index 81e3b05407c0..ef8de182c47b 100644 --- a/libs/WindowManager/Shell/res/values-is/strings.xml +++ b/libs/WindowManager/Shell/res/values-is/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Færa efst til hægri"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Færa neðst til vinstri"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Færðu neðst til hægri"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"stækka valmynd"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"draga saman valmynd"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Færa til vinstri"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Færa til hægri"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"stækka <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"minnka <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Stillingar <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Loka blöðru"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Færa í allan skjáinn"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ekki setja samtal í blöðru"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Spjalla með blöðrum"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Ný samtöl birtast sem fljótandi tákn eða blöðrur. Ýttu til að opna blöðru. Dragðu hana til að færa."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minnka"</string> <string name="close_button_text" msgid="2913281996024033299">"Loka"</string> <string name="back_button_text" msgid="1469718707134137085">"Til baka"</string> - <string name="handle_text" msgid="1766582106752184456">"Handfang"</string> + <string name="handle_text" msgid="4419667835599523257">"Handfang forrits"</string> <string name="app_icon_text" msgid="2823268023931811747">"Tákn forrits"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Allur skjárinn"</string> <string name="desktop_text" msgid="1077633567027630454">"Skjáborðsstilling"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Reikult"</string> <string name="select_text" msgid="5139083974039906583">"Velja"</string> <string name="screenshot_text" msgid="1477704010087786671">"Skjámynd"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Opna í vafra"</string> + <string name="new_window_text" msgid="6318648868380652280">"Nýr gluggi"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Stjórna gluggum"</string> <string name="close_text" msgid="4986518933445178928">"Loka"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Loka valmynd"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Opna valmynd"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Opna valmynd"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Stækka skjá"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Smelluskjár"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Ekki er hægt að breyta stærð þessa forrits"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Stækka"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Smella til vinstri"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Smella til hægri"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml index dc1cbaed13db..8ffc48655327 100644 --- a/libs/WindowManager/Shell/res/values-it/strings.xml +++ b/libs/WindowManager/Shell/res/values-it/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Sposta in alto a destra"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Sposta in basso a sinistra"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Sposta in basso a destra"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"espandi menu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"comprimi menu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Sposta a sinistra"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Sposta a destra"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"espandi <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"comprimi <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Impostazioni <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ignora bolla"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Passa a schermo intero"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Non mettere la conversazione nella bolla"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatta utilizzando le bolle"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Le nuove conversazioni vengono mostrate come icone mobili o bolle. Tocca per aprire la bolla. Trascinala per spostarla."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Riduci a icona"</string> <string name="close_button_text" msgid="2913281996024033299">"Chiudi"</string> <string name="back_button_text" msgid="1469718707134137085">"Indietro"</string> - <string name="handle_text" msgid="1766582106752184456">"Handle"</string> + <string name="handle_text" msgid="4419667835599523257">"Punto di manipolazione app"</string> <string name="app_icon_text" msgid="2823268023931811747">"Icona dell\'app"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Schermo intero"</string> <string name="desktop_text" msgid="1077633567027630454">"Modalità desktop"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Mobile"</string> <string name="select_text" msgid="5139083974039906583">"Seleziona"</string> <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Apri nel browser"</string> + <string name="new_window_text" msgid="6318648868380652280">"Nuova finestra"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Gestisci finestre"</string> <string name="close_text" msgid="4986518933445178928">"Chiudi"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Chiudi il menu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Apri menu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Apri il menu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Massimizza schermo"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Aggancia schermo"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Non è possibile ridimensionare questa app"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Ingrandisci"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Aggancia a sinistra"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Aggancia a destra"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml index 70fc890686f7..a910fd589ef0 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"העברה לפינה הימנית העליונה"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"העברה לפינה השמאלית התחתונה"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"העברה לפינה הימנית התחתונה"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"הרחבת התפריט"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"כיווץ התפריט"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"הזזה שמאלה"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"הזזה ימינה"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"הרחבה של <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"כיווץ של <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"הגדרות <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"סגירת בועה"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"הצגה במסך מלא"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"אין להציג בועות לשיחה"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"לדבר בבועות"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"שיחות חדשות מופיעות כסמלים צפים, או בועות. יש להקיש כדי לפתוח בועה. יש לגרור כדי להזיז אותה."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"מזעור"</string> <string name="close_button_text" msgid="2913281996024033299">"סגירה"</string> <string name="back_button_text" msgid="1469718707134137085">"חזרה"</string> - <string name="handle_text" msgid="1766582106752184456">"נקודת אחיזה"</string> + <string name="handle_text" msgid="4419667835599523257">"נקודת אחיזה לאפליקציה"</string> <string name="app_icon_text" msgid="2823268023931811747">"סמל האפליקציה"</string> <string name="fullscreen_text" msgid="1162316685217676079">"מסך מלא"</string> <string name="desktop_text" msgid="1077633567027630454">"ממשק המחשב"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"בלונים"</string> <string name="select_text" msgid="5139083974039906583">"בחירה"</string> <string name="screenshot_text" msgid="1477704010087786671">"צילום מסך"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"פתיחה בדפדפן"</string> + <string name="new_window_text" msgid="6318648868380652280">"חלון חדש"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"ניהול החלונות"</string> <string name="close_text" msgid="4986518933445178928">"סגירה"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"סגירת התפריט"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"פתיחת התפריט"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"פתיחת התפריט"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"הגדלת המסך"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"כיווץ המסך"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"לא ניתן לשנות את גודל החלון של האפליקציה הזו"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"הגדלה"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"הצמדה לשמאל"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"הצמדה לימין"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ja/strings.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml index 865b38ba1295..33b6ddfff85d 100644 --- a/libs/WindowManager/Shell/res/values-ja/strings.xml +++ b/libs/WindowManager/Shell/res/values-ja/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"右上に移動"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"左下に移動"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"右下に移動"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"メニューを開く"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"メニューを閉じる"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"左に移動"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"右に移動"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>を開きます"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>を閉じます"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> の設定"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"バブルを閉じる"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"全画面表示に移動する"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"会話をバブルで表示しない"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"チャットでバブルを使う"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"新しい会話はフローティング アイコン(バブル)として表示されます。タップするとバブルが開きます。ドラッグしてバブルを移動できます。"</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"最小化"</string> <string name="close_button_text" msgid="2913281996024033299">"閉じる"</string> <string name="back_button_text" msgid="1469718707134137085">"戻る"</string> - <string name="handle_text" msgid="1766582106752184456">"ハンドル"</string> + <string name="handle_text" msgid="4419667835599523257">"アプリハンドル"</string> <string name="app_icon_text" msgid="2823268023931811747">"アプリのアイコン"</string> <string name="fullscreen_text" msgid="1162316685217676079">"全画面表示"</string> <string name="desktop_text" msgid="1077633567027630454">"デスクトップ モード"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"フローティング"</string> <string name="select_text" msgid="5139083974039906583">"選択"</string> <string name="screenshot_text" msgid="1477704010087786671">"スクリーンショット"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"ブラウザで開く"</string> + <string name="new_window_text" msgid="6318648868380652280">"新しいウィンドウ"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"ウィンドウを管理する"</string> <string name="close_text" msgid="4986518933445178928">"閉じる"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"メニューを閉じる"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"メニューを開く"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"メニューを開く"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"画面の最大化"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"画面のスナップ"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"このアプリはサイズ変更できません"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"最大化"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"左にスナップ"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"右にスナップ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml index 3015deccd8d4..3e1f726ca0ef 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"გადაანაცვლეთ ზევით და მარჯვნივ"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ქვევით და მარცხნივ გადატანა"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"გადაანაცვ. ქვემოთ და მარჯვნივ"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"მენიუს გაფართოება"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"მენიუს ჩაკეცვა"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"მარცხნივ გადატანა"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"მარჯვნივ გადატანა"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>-ის გაფართოება"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>-ის ჩაკეცვა"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>-ის პარამეტრები"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"ბუშტის დახურვა"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"სრულეკრანიან რეჟიმზე გადატანა"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"აიკრძალოს საუბრის ბუშტები"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"ჩეთი ბუშტების გამოყენებით"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"ახალი საუბრები გამოჩნდება როგორც მოტივტივე ხატულები ან ბუშტები. შეეხეთ ბუშტის გასახსნელად. გადაიტანეთ ჩავლებით."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"ჩაკეცვა"</string> <string name="close_button_text" msgid="2913281996024033299">"დახურვა"</string> <string name="back_button_text" msgid="1469718707134137085">"უკან"</string> - <string name="handle_text" msgid="1766582106752184456">"იდენტიფიკატორი"</string> + <string name="handle_text" msgid="4419667835599523257">"აპის იდენტიფიკატორი"</string> <string name="app_icon_text" msgid="2823268023931811747">"აპის ხატულა"</string> <string name="fullscreen_text" msgid="1162316685217676079">"სრულ ეკრანზე"</string> <string name="desktop_text" msgid="1077633567027630454">"დესკტოპის რეჟიმი"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"ფარფატი"</string> <string name="select_text" msgid="5139083974039906583">"არჩევა"</string> <string name="screenshot_text" msgid="1477704010087786671">"ეკრანის ანაბეჭდი"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"ბრაუზერში გახსნა"</string> + <string name="new_window_text" msgid="6318648868380652280">"ახალი ფანჯარა"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"ფანჯრების მართვა"</string> <string name="close_text" msgid="4986518933445178928">"დახურვა"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"მენიუს დახურვა"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"მენიუს გახსნა"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"მენიუს გახსნა"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"აპლიკაციის გაშლა სრულ ეკრანზე"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"აპლიკაციის დაპატარავება ეკრანზე"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"აპის ზომის შეცვლა შეუძლებელია"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"მაქსიმალურად გაშლა"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"მარცხნივ გადატანა"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"მარჯვნივ გადატანა"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml index f91175a9bf19..cf39fa87ed1d 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Жоғары оң жаққа жылжыту"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Төменгі сол жаққа жылжыту"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Төменгі оң жаққа жылжыту"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"мәзірді жаю"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"мәзірді жию"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Солға жылжыту"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Оңға жылжыту"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>: жаю"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>: жию"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> параметрлері"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Қалқымалы хабарды жабу"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Толық экранға ауысу"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Әңгіменің қалқыма хабары көрсетілмесін"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Қалқыма хабарлар арқылы сөйлесу"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Жаңа әңгімелер қалқыма белгішелер немесе хабарлар түрінде көрсетіледі. Қалқыма хабарды ашу үшін түртіңіз. Жылжыту үшін сүйреңіз."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Кішірейту"</string> <string name="close_button_text" msgid="2913281996024033299">"Жабу"</string> <string name="back_button_text" msgid="1469718707134137085">"Артқа"</string> - <string name="handle_text" msgid="1766582106752184456">"Идентификатор"</string> + <string name="handle_text" msgid="4419667835599523257">"Қолданба идентификаторы"</string> <string name="app_icon_text" msgid="2823268023931811747">"Қолданба белгішесі"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Толық экран"</string> <string name="desktop_text" msgid="1077633567027630454">"Компьютер режимі"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Қалқыма"</string> <string name="select_text" msgid="5139083974039906583">"Таңдау"</string> <string name="screenshot_text" msgid="1477704010087786671">"Скриншот"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Браузерден ашу"</string> + <string name="new_window_text" msgid="6318648868380652280">"Жаңа терезе"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Терезелерді басқару"</string> <string name="close_text" msgid="4986518933445178928">"Жабу"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Мәзірді жабу"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Мәзірді ашу"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Мәзірді ашу"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Экранды ұлғайту"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Экранды бөлу"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Бұл қолданбаның өлшемі өзгертілмейді."</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Жаю"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Солға тіркеу"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Оңға тіркеу"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml index 42467b40e47d..0514c4ea2a28 100644 --- a/libs/WindowManager/Shell/res/values-km/strings.xml +++ b/libs/WindowManager/Shell/res/values-km/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ផ្លាស់ទីទៅផ្នែកខាងលើខាងស្ដាំ"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ផ្លាស់ទីទៅផ្នែកខាងក្រោមខាងឆ្វេង"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ផ្លាស់ទីទៅផ្នែកខាងក្រោមខាងស្ដាំ"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"ពង្រីកម៉ឺនុយ"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"បង្រួមម៉ឺនុយ"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"ផ្លាស់ទីទៅឆ្វេង"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"ផ្លាស់ទីទៅស្តាំ"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"ពង្រីក <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"បង្រួម <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"ការកំណត់ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"ច្រានចោលពពុះ"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"ផ្លាស់ទីទៅអេក្រង់ពេញ"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"កុំបង្ហាញការសន្ទនាជាពពុះ"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"ជជែកដោយប្រើពពុះ"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"ការសន្ទនាថ្មីៗបង្ហាញជាពពុះ ឬរូបអណ្ដែត។ ចុច ដើម្បីបើកពពុះ។ អូស ដើម្បីផ្លាស់ទីពពុះនេះ។"</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"បង្រួម"</string> <string name="close_button_text" msgid="2913281996024033299">"បិទ"</string> <string name="back_button_text" msgid="1469718707134137085">"ថយក្រោយ"</string> - <string name="handle_text" msgid="1766582106752184456">"ឈ្មោះអ្នកប្រើប្រាស់"</string> + <string name="handle_text" msgid="4419667835599523257">"ឈ្មោះអ្នកប្រើប្រាស់កម្មវិធី"</string> <string name="app_icon_text" msgid="2823268023931811747">"រូបកម្មវិធី"</string> <string name="fullscreen_text" msgid="1162316685217676079">"អេក្រង់ពេញ"</string> <string name="desktop_text" msgid="1077633567027630454">"មុខងារកុំព្យូទ័រ"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"អណ្ដែត"</string> <string name="select_text" msgid="5139083974039906583">"ជ្រើសរើស"</string> <string name="screenshot_text" msgid="1477704010087786671">"រូបថតអេក្រង់"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"បើកក្នុងកម្មវិធីរុករកតាមអ៊ីនធឺណិត"</string> + <string name="new_window_text" msgid="6318648868380652280">"វិនដូថ្មី"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"គ្រប់គ្រងវិនដូ"</string> <string name="close_text" msgid="4986518933445178928">"បិទ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"បិទម៉ឺនុយ"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"បើកម៉ឺនុយ"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"បើកម៉ឺនុយ"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ពង្រីកអេក្រង់"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ថតអេក្រង់"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"មិនអាចប្ដូរទំហំកម្មវិធីនេះបានទេ"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"ពង្រីក"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ផ្លាស់ទីទៅឆ្វេង"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"ផ្លាស់ទីទៅស្ដាំ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-kn/strings.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml index 9df36b7ada96..4d65fa9c78ab 100644 --- a/libs/WindowManager/Shell/res/values-kn/strings.xml +++ b/libs/WindowManager/Shell/res/values-kn/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ಬಲ ಮೇಲ್ಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ಸ್ಕ್ರೀನ್ನ ಎಡ ಕೆಳಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ಕೆಳಗಿನ ಬಲಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"ಮೆನು ವಿಸ್ತರಿಸಿ"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"ಮೆನು ಸಂಕುಚಿಸಿ"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"ಎಡಕ್ಕೆ ಸರಿಸಿ"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"ಬಲಕ್ಕೆ ಸರಿಸಿ"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> ಅನ್ನು ವಿಸ್ತೃತಗೊಳಿಸಿ"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> ಅನ್ನು ಕುಗ್ಗಿಸಿ"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ಸೆಟ್ಟಿಂಗ್ಗಳು"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"ಬಬಲ್ ವಜಾಗೊಳಿಸಿ"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"ಫುಲ್ಸ್ಕ್ರೀನ್ಗೆ ಸರಿಸಿ"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"ಸಂಭಾಷಣೆಯನ್ನು ಬಬಲ್ ಮಾಡಬೇಡಿ"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"ಬಬಲ್ಸ್ ಬಳಸಿ ಚಾಟ್ ಮಾಡಿ"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"ಹೊಸ ಸಂಭಾಷಣೆಗಳು ತೇಲುವ ಐಕಾನ್ಗಳು ಅಥವಾ ಬಬಲ್ಸ್ ಆಗಿ ಗೋಚರಿಸುತ್ತವೆ. ಬಬಲ್ ತೆರೆಯಲು ಟ್ಯಾಪ್ ಮಾಡಿ. ಅದನ್ನು ಡ್ರ್ಯಾಗ್ ಮಾಡಲು ಎಳೆಯಿರಿ."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"ಕುಗ್ಗಿಸಿ"</string> <string name="close_button_text" msgid="2913281996024033299">"ಮುಚ್ಚಿರಿ"</string> <string name="back_button_text" msgid="1469718707134137085">"ಹಿಂದಕ್ಕೆ"</string> - <string name="handle_text" msgid="1766582106752184456">"ಹ್ಯಾಂಡಲ್"</string> + <string name="handle_text" msgid="4419667835599523257">"ಆ್ಯಪ್ ಹ್ಯಾಂಡಲ್"</string> <string name="app_icon_text" msgid="2823268023931811747">"ಆ್ಯಪ್ ಐಕಾನ್"</string> <string name="fullscreen_text" msgid="1162316685217676079">"ಫುಲ್ಸ್ಕ್ರೀನ್"</string> <string name="desktop_text" msgid="1077633567027630454">"ಡೆಸ್ಕ್ಟಾಪ್ ಮೋಡ್"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"ಫ್ಲೋಟ್"</string> <string name="select_text" msgid="5139083974039906583">"ಆಯ್ಕೆಮಾಡಿ"</string> <string name="screenshot_text" msgid="1477704010087786671">"ಸ್ಕ್ರೀನ್ಶಾಟ್"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"ಬ್ರೌಸರ್ನಲ್ಲಿ ತೆರೆಯಿರಿ"</string> + <string name="new_window_text" msgid="6318648868380652280">"ಹೊಸ ವಿಂಡೋ"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"ವಿಂಡೋಗಳನ್ನು ನಿರ್ವಹಿಸಿ"</string> <string name="close_text" msgid="4986518933445178928">"ಮುಚ್ಚಿ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ಮೆನು ಮುಚ್ಚಿ"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"ಮೆನು ತೆರೆಯಿರಿ"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"ಮೆನು ತೆರೆಯಿರಿ"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ಸ್ಕ್ರೀನ್ ಅನ್ನು ಮ್ಯಾಕ್ಸಿಮೈಸ್ ಮಾಡಿ"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ಸ್ನ್ಯಾಪ್ ಸ್ಕ್ರೀನ್"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"ಈ ಆ್ಯಪ್ ಅನ್ನು ಮರುಗಾತ್ರಗೊಳಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"ಮ್ಯಾಕ್ಸಿಮೈಸ್ ಮಾಡಿ"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ಎಡಕ್ಕೆ ಸ್ನ್ಯಾಪ್ ಮಾಡಿ"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"ಬಲಕ್ಕೆ ಸ್ನ್ಯಾಪ್ ಮಾಡಿ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml index a1f9934b89ba..21319663a700 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"오른쪽 상단으로 이동"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"왼쪽 하단으로 이동"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"오른쪽 하단으로 이동"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"메뉴 펼치기"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"메뉴 접기"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"왼쪽으로 이동"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"오른쪽으로 이동"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> 펼치기"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> 접기"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> 설정"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"대화창 닫기"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"전체 화면으로 이동"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"대화를 대화창으로 표시하지 않기"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"대화창으로 채팅하기"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"새로운 대화가 플로팅 아이콘인 대화창으로 표시됩니다. 대화창을 열려면 탭하세요. 드래그하여 이동할 수 있습니다."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"최소화"</string> <string name="close_button_text" msgid="2913281996024033299">"닫기"</string> <string name="back_button_text" msgid="1469718707134137085">"뒤로"</string> - <string name="handle_text" msgid="1766582106752184456">"핸들"</string> + <string name="handle_text" msgid="4419667835599523257">"앱 핸들"</string> <string name="app_icon_text" msgid="2823268023931811747">"앱 아이콘"</string> <string name="fullscreen_text" msgid="1162316685217676079">"전체 화면"</string> <string name="desktop_text" msgid="1077633567027630454">"데스크톱 모드"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"플로팅"</string> <string name="select_text" msgid="5139083974039906583">"선택"</string> <string name="screenshot_text" msgid="1477704010087786671">"스크린샷"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"브라우저에서 열기"</string> + <string name="new_window_text" msgid="6318648868380652280">"새 창"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"창 관리"</string> <string name="close_text" msgid="4986518933445178928">"닫기"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"메뉴 닫기"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"메뉴 열기"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"메뉴 열기"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"화면 최대화"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"화면 분할"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"이 앱은 크기를 조절할 수 없습니다."</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"최대화하기"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"왼쪽으로 맞추기"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"오른쪽으로 맞추기"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml index 14275da0209c..c977799e7fcd 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Жогорку оң жакка жылдыруу"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Төмөнкү сол жакка жылдыруу"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Төмөнкү оң жакка жылдыруу"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"менюну жайып көрсөтүү"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"менюну жыйыштыруу"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Солго жылдыруу"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Оңго жылдыруу"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> жайып көрсөтүү"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> жыйыштыруу"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> параметрлери"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Калкып чыкма билдирмени жабуу"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Толук экранга өтүү"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Жазышууда калкып чыкма билдирмелер көрүнбөсүн"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Калкып чыкма билдирмелер аркылуу маектешүү"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Жаңы жазышуулар калкыма сүрөтчөлөр же калкып чыкма билдирмелер түрүндө көрүнөт. Калкып чыкма билдирмелерди ачуу үчүн тийип коюңуз. Жылдыруу үчүн сүйрөңүз."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Кичирейтүү"</string> <string name="close_button_text" msgid="2913281996024033299">"Жабуу"</string> <string name="back_button_text" msgid="1469718707134137085">"Артка"</string> - <string name="handle_text" msgid="1766582106752184456">"Маркер"</string> + <string name="handle_text" msgid="4419667835599523257">"Колдонмонун маркери"</string> <string name="app_icon_text" msgid="2823268023931811747">"Колдонмонун сүрөтчөсү"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Толук экран"</string> <string name="desktop_text" msgid="1077633567027630454">"Компьютер режими"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Калкыма"</string> <string name="select_text" msgid="5139083974039906583">"Тандоо"</string> <string name="screenshot_text" msgid="1477704010087786671">"Скриншот"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Серепчиден ачуу"</string> + <string name="new_window_text" msgid="6318648868380652280">"Жаңы терезе"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Терезелерди тескөө"</string> <string name="close_text" msgid="4986518933445178928">"Жабуу"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Менюну жабуу"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Менюну ачуу"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Менюну ачуу"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Экранды чоңойтуу"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Экранды сүрөткө тартып алуу"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Бул колдонмонун өлчөмүн өзгөртүүгө болбойт"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Чоңойтуу"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Солго жылдыруу"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Оңго жылдыруу"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml index 48d26364a770..4e10621c719b 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ຍ້າຍຂວາເທິງ"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ຍ້າຍຊ້າຍລຸ່ມ"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ຍ້າຍຂວາລຸ່ມ"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"ຂະຫຍາຍເມນູ"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"ຫຍໍ້ເມນູລົງ"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"ຍ້າຍໄປທາງຊ້າຍ"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"ຍ້າຍໄປທາງຂວາ"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"ຂະຫຍາຍ <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"ຫຍໍ້ <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> ລົງ"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"ການຕັ້ງຄ່າ <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"ປິດຟອງໄວ້"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"ຍ້າຍໄປໂໝດເຕັມຈໍ"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"ຢ່າໃຊ້ຟອງໃນການສົນທະນາ"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"ສົນທະນາໂດຍໃຊ້ຟອງ"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"ການສົນທະນາໃໝ່ຈະປາກົດເປັນໄອຄອນ ຫຼື ຟອງແບບລອຍ. ແຕະເພື່ອເປີດຟອງ. ລາກເພື່ອຍ້າຍມັນ."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"ຫຍໍ້ລົງ"</string> <string name="close_button_text" msgid="2913281996024033299">"ປິດ"</string> <string name="back_button_text" msgid="1469718707134137085">"ກັບຄືນ"</string> - <string name="handle_text" msgid="1766582106752184456">"ມືບັງຄັບ"</string> + <string name="handle_text" msgid="4419667835599523257">"ຊື່ຜູ້ໃຊ້ແອັບ"</string> <string name="app_icon_text" msgid="2823268023931811747">"ໄອຄອນແອັບ"</string> <string name="fullscreen_text" msgid="1162316685217676079">"ເຕັມຈໍ"</string> <string name="desktop_text" msgid="1077633567027630454">"ໂໝດເດັສທັອບ"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"ລອຍ"</string> <string name="select_text" msgid="5139083974039906583">"ເລືອກ"</string> <string name="screenshot_text" msgid="1477704010087786671">"ຮູບໜ້າຈໍ"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"ເປີດໃນໂປຣແກຣມທ່ອງເວັບ"</string> + <string name="new_window_text" msgid="6318648868380652280">"ໜ້າຈໍໃໝ່"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"ຈັດການໜ້າຈໍ"</string> <string name="close_text" msgid="4986518933445178928">"ປິດ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ປິດເມນູ"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"ເປີດເມນູ"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"ເປີດເມນູ"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ປັບຈໍໃຫຍ່ສຸດ"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ສະແນັບໜ້າຈໍ"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"ບໍ່ສາມາດປັບຂະໜາດແອັບນີ້ໄດ້"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"ຂະຫຍາຍໃຫຍ່ສຸດ"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ແນບຊ້າຍ"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"ແນບຂວາ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml index cf0034f1cce7..6d3c58cf01bb 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Perkelti į viršų dešinėje"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Perkelti į apačią kairėje"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Perkelti į apačią dešinėje"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"išskleisti meniu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"sutraukti meniu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Perkelti kairėn"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Perkelti dešinėn"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"išskleisti „<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>“"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"sutraukti „<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>“"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"„<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>“ nustatymai"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Atsisakyti burbulo"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Pereiti į viso ekrano režimą"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nerodyti pokalbio burbule"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Pokalbis naudojant burbulus"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nauji pokalbiai rodomi kaip slankiosios piktogramos arba burbulai. Palieskite, kad atidarytumėte burbulą. Vilkite, kad perkeltumėte."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Sumažinti"</string> <string name="close_button_text" msgid="2913281996024033299">"Uždaryti"</string> <string name="back_button_text" msgid="1469718707134137085">"Atgal"</string> - <string name="handle_text" msgid="1766582106752184456">"Rankenėlė"</string> + <string name="handle_text" msgid="4419667835599523257">"Programos kreipinys"</string> <string name="app_icon_text" msgid="2823268023931811747">"Programos piktograma"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Visas ekranas"</string> <string name="desktop_text" msgid="1077633567027630454">"Stalinio kompiuterio režimas"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Slankusis langas"</string> <string name="select_text" msgid="5139083974039906583">"Pasirinkti"</string> <string name="screenshot_text" msgid="1477704010087786671">"Ekrano kopija"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Atidaryti naršyklėje"</string> + <string name="new_window_text" msgid="6318648868380652280">"Naujas langas"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Tvarkyti langus"</string> <string name="close_text" msgid="4986518933445178928">"Uždaryti"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Uždaryti meniu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Atidaryti meniu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Atidaryti meniu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Išskleisti ekraną"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Sutraukti ekraną"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Negalima keisti šios programos dydžio"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Padidinti"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Pritraukti kairėje"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Pritraukti dešinėje"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml index bcd71cd71f0c..e82a80a2dc74 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Pārvietot augšpusē pa labi"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Pārvietot apakšpusē pa kreisi"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Pārvietot apakšpusē pa labi"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"izvērst izvēlni"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"sakļaut izvēlni"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Pārvietot pa kreisi"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Pārvietot pa labi"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"Izvērst “<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>”"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"Sakļaut “<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>”"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Lietotnes <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> iestatījumi"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Nerādīt burbuli"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Pārvietot uz pilnekrāna režīmu"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nerādīt sarunu burbuļos"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Tērzēšana, izmantojot burbuļus"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Jaunas sarunas tiek rādītas kā peldošas ikonas vai burbuļi. Pieskarieties, lai atvērtu burbuli. Velciet, lai to pārvietotu."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimizēt"</string> <string name="close_button_text" msgid="2913281996024033299">"Aizvērt"</string> <string name="back_button_text" msgid="1469718707134137085">"Atpakaļ"</string> - <string name="handle_text" msgid="1766582106752184456">"Turis"</string> + <string name="handle_text" msgid="4419667835599523257">"Lietotnes turis"</string> <string name="app_icon_text" msgid="2823268023931811747">"Lietotnes ikona"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Pilnekrāna režīms"</string> <string name="desktop_text" msgid="1077633567027630454">"Darbvirsmas režīms"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Peldošs"</string> <string name="select_text" msgid="5139083974039906583">"Atlasīt"</string> <string name="screenshot_text" msgid="1477704010087786671">"Ekrānuzņēmums"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Atvērt pārlūkā"</string> + <string name="new_window_text" msgid="6318648868380652280">"Jauns logs"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Pārvaldīt logus"</string> <string name="close_text" msgid="4986518933445178928">"Aizvērt"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Aizvērt izvēlni"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Atvērt izvēlni"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Atvērt izvēlni"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimizēt ekrānu"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fiksēt ekrānu"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Šīs lietotnes loga lielumu nevar mainīt."</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimizēt"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Piestiprināt pa kreisi"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Piestiprināt pa labi"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mk/strings.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml index 48fca3e5a2a7..0e77234cf411 100644 --- a/libs/WindowManager/Shell/res/values-mk/strings.xml +++ b/libs/WindowManager/Shell/res/values-mk/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Премести горе десно"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Премести долу лево"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Премести долу десно"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"го проширува менито"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"го собира менито"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Преместете налево"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Преместете надесно"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"прошири <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"собери <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Поставки за <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Отфрли балонче"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Префрлете на цел екран"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Не прикажувај го разговорот во балончиња"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Разговор во балончиња"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Новите разговори ќе се појавуваат како лебдечки икони или балончиња. Допрете за отворање на балончето. Повлечете за да го преместите."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Минимизирај"</string> <string name="close_button_text" msgid="2913281996024033299">"Затвори"</string> <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> - <string name="handle_text" msgid="1766582106752184456">"Прекар"</string> + <string name="handle_text" msgid="4419667835599523257">"Прекар на апликацијата"</string> <string name="app_icon_text" msgid="2823268023931811747">"Икона на апликацијата"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Цел екран"</string> <string name="desktop_text" msgid="1077633567027630454">"Режим за компјутер"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Лебдечко"</string> <string name="select_text" msgid="5139083974039906583">"Изберете"</string> <string name="screenshot_text" msgid="1477704010087786671">"Слика од екранот"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Отвори во прелистувач"</string> + <string name="new_window_text" msgid="6318648868380652280">"Нов прозорец"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Управувајте со прозорци"</string> <string name="close_text" msgid="4986518933445178928">"Затворете"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Затворете го менито"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Отвори го менито"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Отвори го менито"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Максимизирај го екранот"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Подели го екранот на половина"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Не може да се промени големината на апликацијава"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Максимизирај"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Фотографирај лево"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Фотографирај десно"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml index 1e7be913a2d7..a48df0b71b1b 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"മുകളിൽ വലതുഭാഗത്തേക്ക് നീക്കുക"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ചുവടെ ഇടതുഭാഗത്തേക്ക് നീക്കുക"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ചുവടെ വലതുഭാഗത്തേക്ക് നീക്കുക"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"മെനു വികസിപ്പിക്കുക"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"മെനു ചുരുക്കുക"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"ഇടത്തേക്ക് നീക്കുക"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"വലത്തേക്ക് നീക്കുക"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> വികസിപ്പിക്കുക"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> ചുരുക്കുക"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ക്രമീകരണം"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"ബബിൾ ഡിസ്മിസ് ചെയ്യൂ"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"പൂർണ്ണസ്ക്രീനിലേക്ക് നീങ്ങുക"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"സംഭാഷണം ബബിൾ ചെയ്യരുത്"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"ബബിളുകൾ ഉപയോഗിച്ച് ചാറ്റ് ചെയ്യുക"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"പുതിയ സംഭാഷണങ്ങൾ ഫ്ലോട്ടിംഗ് ഐക്കണുകളോ ബബിളുകളോ ആയി ദൃശ്യമാവുന്നു. ബബിൾ തുറക്കാൻ ടാപ്പ് ചെയ്യൂ. ഇത് നീക്കാൻ വലിച്ചിടുക."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"ചെറുതാക്കുക"</string> <string name="close_button_text" msgid="2913281996024033299">"അടയ്ക്കുക"</string> <string name="back_button_text" msgid="1469718707134137085">"മടങ്ങുക"</string> - <string name="handle_text" msgid="1766582106752184456">"ഹാൻഡിൽ"</string> + <string name="handle_text" msgid="4419667835599523257">"ആപ്പ് ഹാൻഡിൽ"</string> <string name="app_icon_text" msgid="2823268023931811747">"ആപ്പ് ഐക്കൺ"</string> <string name="fullscreen_text" msgid="1162316685217676079">"പൂർണ്ണസ്ക്രീൻ"</string> <string name="desktop_text" msgid="1077633567027630454">"ഡെസ്ക്ടോപ്പ് മോഡ്"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"ഫ്ലോട്ട്"</string> <string name="select_text" msgid="5139083974039906583">"തിരഞ്ഞെടുക്കുക"</string> <string name="screenshot_text" msgid="1477704010087786671">"സ്ക്രീൻഷോട്ട്"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"ബ്രൗസറിൽ തുറക്കുക"</string> + <string name="new_window_text" msgid="6318648868380652280">"പുതിയ വിന്ഡോ"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"വിൻഡോകൾ മാനേജ് ചെയ്യുക"</string> <string name="close_text" msgid="4986518933445178928">"അടയ്ക്കുക"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"മെനു അടയ്ക്കുക"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"മെനു തുറക്കുക"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"മെനു തുറക്കുക"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"സ്ക്രീൻ വലുതാക്കുക"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"സ്ക്രീൻ സ്നാപ്പ് ചെയ്യുക"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"ഈ ആപ്പിന്റെ വലുപ്പം മാറ്റാനാകില്ല"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"വലുതാക്കുക"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ഇടതുവശത്തേക്ക് സ്നാപ്പ് ചെയ്യുക"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"വലതുവശത്തേക്ക് സ്നാപ്പ് ചെയ്യുക"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mn/strings.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml index e65494d72935..e317df582d33 100644 --- a/libs/WindowManager/Shell/res/values-mn/strings.xml +++ b/libs/WindowManager/Shell/res/values-mn/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Баруун дээш зөөх"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Зүүн доош зөөх"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Баруун доош зөөх"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"цэсийг дэлгэх"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"цэсийг хураах"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Зүүн тийш зөөх"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Баруун тийш зөөх"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>-г дэлгэх"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>-г хураах"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>-н тохиргоо"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Бөмбөлгийг хаах"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Бүтэн дэлгэц рүү очих"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Харилцан яриаг бүү бөмбөлөг болго"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Бөмбөлөг ашиглан чатлаарай"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Шинэ харилцан яриа нь хөвөгч дүрс тэмдэг эсвэл бөмбөлөг хэлбэрээр харагддаг. Бөмбөлгийг нээхийн тулд товшино уу. Түүнийг зөөхийн тулд чирнэ үү."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Багасгах"</string> <string name="close_button_text" msgid="2913281996024033299">"Хаах"</string> <string name="back_button_text" msgid="1469718707134137085">"Буцах"</string> - <string name="handle_text" msgid="1766582106752184456">"Бариул"</string> + <string name="handle_text" msgid="4419667835599523257">"Аппын бариул"</string> <string name="app_icon_text" msgid="2823268023931811747">"Aппын дүрс тэмдэг"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Бүтэн дэлгэц"</string> <string name="desktop_text" msgid="1077633567027630454">"Дэлгэцийн горим"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Хөвөгч"</string> <string name="select_text" msgid="5139083974039906583">"Сонгох"</string> <string name="screenshot_text" msgid="1477704010087786671">"Дэлгэцийн агшин"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Хөтчид нээх"</string> + <string name="new_window_text" msgid="6318648868380652280">"Шинэ цонх"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Windows-г удирдах"</string> <string name="close_text" msgid="4986518933445178928">"Хаах"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Цэсийг хаах"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Цэс нээх"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Цэсийг нээх"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Дэлгэцийг томруулах"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Дэлгэцийг таллах"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Энэ аппын хэмжээг өөрчлөх боломжгүй"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Томруулах"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Зүүн тийш зэрэгцүүлэх"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Баруун тийш зэрэгцүүлэх"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml index 220969047b37..cfe43d377ab1 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"वर उजवीकडे हलवा"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"तळाशी डावीकडे हलवा"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"तळाशी उजवीकडे हलवा"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"मेनूचा विस्तार करा"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"मेनू कोलॅप्स करा"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"डावीकडे हलवा"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"उजवीकडे हलवा"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> विस्तार करा"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> कोलॅप्स करा"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> सेटिंग्ज"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"बबल डिसमिस करा"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"फुलस्क्रीनवर हलवा"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"संभाषणाला बबल करू नका"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"बबल वापरून चॅट करा"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"नवीन संभाषणे फ्लोटिंग आयकन किंवा बबल म्हणून दिसतात. बबल उघडण्यासाठी टॅप करा. हे हलवण्यासाठी ड्रॅग करा."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"लहान करा"</string> <string name="close_button_text" msgid="2913281996024033299">"बंद करा"</string> <string name="back_button_text" msgid="1469718707134137085">"मागे जा"</string> - <string name="handle_text" msgid="1766582106752184456">"हँडल"</string> + <string name="handle_text" msgid="4419667835599523257">"अॅपचे हँडल"</string> <string name="app_icon_text" msgid="2823268023931811747">"अॅप आयकन"</string> <string name="fullscreen_text" msgid="1162316685217676079">"फुलस्क्रीन"</string> <string name="desktop_text" msgid="1077633567027630454">"डेस्कटॉप मोड"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"फ्लोट"</string> <string name="select_text" msgid="5139083974039906583">"निवडा"</string> <string name="screenshot_text" msgid="1477704010087786671">"स्क्रीनशॉट"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"ब्राउझरमध्ये उघडा"</string> + <string name="new_window_text" msgid="6318648868380652280">"नवीन विंडो"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"विंडो व्यवस्थापित करा"</string> <string name="close_text" msgid="4986518933445178928">"बंद करा"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"मेनू बंद करा"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"मेनू उघडा"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"मेनू उघडा"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"स्क्रीन मोठी करा"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"स्क्रीन स्नॅप करा"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"या अॅपचा आकार बदलला जाऊ शकत नाही"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"मोठे करा"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"डावीकडे स्नॅप करा"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"उजवीकडे स्नॅप करा"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ms/strings.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml index 32353c4585c6..15ccfbbad39f 100644 --- a/libs/WindowManager/Shell/res/values-ms/strings.xml +++ b/libs/WindowManager/Shell/res/values-ms/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Alihkan ke atas sebelah kanan"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Alihkan ke bawah sebelah kiri"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Alihkan ke bawah sebelah kanan"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"kembangkan menu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"kuncupkan menu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Alih ke kiri"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Alih ke kanan"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"kembangkan <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"kuncupkan <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Tetapan <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ketepikan gelembung"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Alihkan kepada skrin penuh"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Jangan jadikan perbualan dalam bentuk gelembung"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Bersembang menggunakan gelembung"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Perbualan baharu muncul sebagai ikon terapung atau gelembung. Ketik untuk membuka gelembung. Seret untuk mengalihkan gelembung tersebut."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimumkan"</string> <string name="close_button_text" msgid="2913281996024033299">"Tutup"</string> <string name="back_button_text" msgid="1469718707134137085">"Kembali"</string> - <string name="handle_text" msgid="1766582106752184456">"Pemegang"</string> + <string name="handle_text" msgid="4419667835599523257">"Pengendalian apl"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ikon Apl"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Skrin penuh"</string> <string name="desktop_text" msgid="1077633567027630454">"Mod Desktop"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Terapung"</string> <string name="select_text" msgid="5139083974039906583">"Pilih"</string> <string name="screenshot_text" msgid="1477704010087786671">"Tangkapan skrin"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Buka dalam penyemak imbas"</string> + <string name="new_window_text" msgid="6318648868380652280">"Tetingkap Baharu"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Urus Tetingkap"</string> <string name="close_text" msgid="4986518933445178928">"Tutup"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Tutup Menu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Buka Menu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Buka Menu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimumkan Skrin"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Tangkap Skrin"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Apl ini tidak boleh diubah saiz"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimumkan"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Autojajar ke kiri"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Autojajar ke kanan"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-my/strings.xml b/libs/WindowManager/Shell/res/values-my/strings.xml index fadb67ed5ef9..f6d8b1f3e726 100644 --- a/libs/WindowManager/Shell/res/values-my/strings.xml +++ b/libs/WindowManager/Shell/res/values-my/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ညာဘက်ထိပ်သို့ ရွှေ့ပါ"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ဘယ်အောက်ခြေသို့ ရွှေ့ရန်"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ညာအောက်ခြေသို့ ရွှေ့ပါ"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"မီနူးကို ပိုပြပါ"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"မီနူးကို လျှော့ပြပါ"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"ဘယ်သို့ရွှေ့ရန်"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"ညာသို့ရွှေ့ရန်"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> ကို ချဲ့ရန်"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> ကို ချုံ့ရန်"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ဆက်တင်များ"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"ပူဖောင်းကွက် ပယ်ရန်"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"ဖန်သားပြင်အပြည့်သို့ ရွှေ့ရန်"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"စကားဝိုင်းကို ပူဖောင်းကွက် မပြုလုပ်ပါနှင့်"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"ပူဖောင်းကွက် သုံး၍ ချတ်လုပ်ခြင်း"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"စကားဝိုင်းအသစ်များကို မျောနေသည့် သင်္ကေတများ သို့မဟုတ် ပူဖောင်းကွက်များအဖြစ် မြင်ရပါမည်။ ပူဖောင်းကွက်ကိုဖွင့်ရန် တို့ပါ။ ရွှေ့ရန် ၎င်းကို ဖိဆွဲပါ။"</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"ချုံ့ရန်"</string> <string name="close_button_text" msgid="2913281996024033299">"ပိတ်ရန်"</string> <string name="back_button_text" msgid="1469718707134137085">"နောက်သို့"</string> - <string name="handle_text" msgid="1766582106752184456">"သုံးသူအမည်"</string> + <string name="handle_text" msgid="4419667835599523257">"အက်ပ်သုံးသူအမည်"</string> <string name="app_icon_text" msgid="2823268023931811747">"အက်ပ်သင်္ကေတ"</string> <string name="fullscreen_text" msgid="1162316685217676079">"ဖန်သားပြင်အပြည့်"</string> <string name="desktop_text" msgid="1077633567027630454">"ဒက်စ်တော့မုဒ်"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"မျှောရန်"</string> <string name="select_text" msgid="5139083974039906583">"ရွေးရန်"</string> <string name="screenshot_text" msgid="1477704010087786671">"ဖန်သားပြင်ဓာတ်ပုံ"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"ဘရောင်ဇာတွင် ဖွင့်ရန်"</string> + <string name="new_window_text" msgid="6318648868380652280">"ဝင်းဒိုးအသစ်"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"ဝင်းဒိုးများ စီမံရန်"</string> <string name="close_text" msgid="4986518933445178928">"ပိတ်ရန်"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"မီနူး ပိတ်ရန်"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"မီနူး ဖွင့်ရန်"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"မီနူး ဖွင့်ရန်"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"စခရင်ကို ချဲ့မည်"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"စခရင်ကို ချုံ့မည်"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"ဤအက်ပ်ကို အရွယ်ပြင်၍ မရပါ"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"ချဲ့ရန်"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ဘယ်တွင် ချဲ့ရန်"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"ညာတွင် ချဲ့ရန်"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml index 76d833f1b6e5..4e8656733c1d 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Flytt til øverst til høyre"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Flytt til nederst til venstre"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Flytt til nederst til høyre"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"vis menyen"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"skjul menyen"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Flytt til venstre"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Flytt til høyre"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"vis <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"skjul <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>-innstillinger"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Lukk boblen"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Flytt til fullskjerm"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ikke vis samtaler i bobler"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat med bobler"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nye samtaler vises som flytende ikoner eller bobler. Trykk for å åpne en boble. Dra for å flytte den."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimer"</string> <string name="close_button_text" msgid="2913281996024033299">"Lukk"</string> <string name="back_button_text" msgid="1469718707134137085">"Tilbake"</string> - <string name="handle_text" msgid="1766582106752184456">"Håndtak"</string> + <string name="handle_text" msgid="4419667835599523257">"Apphåndtak"</string> <string name="app_icon_text" msgid="2823268023931811747">"Appikon"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Fullskjerm"</string> <string name="desktop_text" msgid="1077633567027630454">"Skrivebordmodus"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Svevende"</string> <string name="select_text" msgid="5139083974039906583">"Velg"</string> <string name="screenshot_text" msgid="1477704010087786671">"Skjermbilde"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Åpne i nettleseren"</string> + <string name="new_window_text" msgid="6318648868380652280">"Nytt vindu"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Administrer vinduene"</string> <string name="close_text" msgid="4986518933445178928">"Lukk"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Lukk menyen"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Åpne menyen"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Åpne menyen"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimer skjermen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fest skjermen"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Du kan ikke endre størrelse på denne appen"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimer"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Fest til venstre"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Fest til høyre"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml index c5a4b35b7aa8..cf579e1dc33e 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"सिरानमा दायाँतिर सार्नुहोस्"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"पुछारमा बायाँतिर सार्नुहोस्"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"पुछारमा दायाँतिर सार्नुहोस्"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"मेनु एक्स्पान्ड गर्नुहोस्"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"मेनु कोल्याप्स गर्नुहोस्"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"बायाँतिर सार्नुहोस्"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"दायाँतिर सार्नुहोस्"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> एक्स्पान्ड गर्नुहोस्"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> कोल्याप्स गर्नुहोस्"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> का सेटिङहरू"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"बबल खारेज गर्नुहोस्"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"सारेर फुल स्क्रिनमा लैजानुहोस्"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"वार्तालाप बबलको रूपमा नदेखाउनुहोस्"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"बबलहरू प्रयोग गरी कुराकानी गर्नुहोस्"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"नयाँ वार्तालापहरू तैरने आइकन वा बबलका रूपमा देखिन्छन्। बबल खोल्न ट्याप गर्नुहोस्। बबल सार्न सो बबललाई ड्र्याग गर्नुहोस्।"</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"मिनिमाइज गर्नुहोस्"</string> <string name="close_button_text" msgid="2913281996024033299">"बन्द गर्नुहोस्"</string> <string name="back_button_text" msgid="1469718707134137085">"पछाडि"</string> - <string name="handle_text" msgid="1766582106752184456">"ह्यान्डल"</string> + <string name="handle_text" msgid="4419667835599523257">"एपको ह्यान्डल"</string> <string name="app_icon_text" msgid="2823268023931811747">"एपको आइकन"</string> <string name="fullscreen_text" msgid="1162316685217676079">"फुल स्क्रिन"</string> <string name="desktop_text" msgid="1077633567027630454">"डेस्कटप मोड"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"फ्लोट"</string> <string name="select_text" msgid="5139083974039906583">"चयन गर्नुहोस्"</string> <string name="screenshot_text" msgid="1477704010087786671">"स्क्रिनसट"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"ब्राउजरमा खोल्नुहोस्"</string> + <string name="new_window_text" msgid="6318648868380652280">"नयाँ विन्डो"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"विन्डोहरू व्यवस्थापन गर्नुहोस्"</string> <string name="close_text" msgid="4986518933445178928">"बन्द गर्नुहोस्"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"मेनु बन्द गर्नुहोस्"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"मेनु खोल्नुहोस्"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"मेनु खोल्नुहोस्"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"स्क्रिन ठुलो बनाउनुहोस्"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"स्क्रिन स्न्याप गर्नुहोस्"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"यो एपको आकार बदल्न मिल्दैन"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"ठुलो बनाउनुहोस्"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"बायाँतिर स्न्याप गर्नुहोस्"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"दायाँतिर स्न्याप गर्नुहोस्"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml index 0b9afa4c3836..176bcade3e2b 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Naar rechtsboven verplaatsen"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Naar linksonder verplaatsen"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Naar rechtsonder verplaatsen"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"menu uitvouwen"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"menu samenvouwen"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Naar links verplaatsen"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Naar rechts verplaatsen"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> uitvouwen"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> samenvouwen"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Instellingen voor <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Bubbel sluiten"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Naar volledig scherm"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Gesprekken niet in bubbels tonen"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatten met bubbels"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nieuwe gesprekken worden als zwevende iconen of bubbels getoond. Tik om een bubbel te openen. Sleep om een bubbel te verplaatsen."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimaliseren"</string> <string name="close_button_text" msgid="2913281996024033299">"Sluiten"</string> <string name="back_button_text" msgid="1469718707134137085">"Terug"</string> - <string name="handle_text" msgid="1766582106752184456">"Gebruikersnaam"</string> + <string name="handle_text" msgid="4419667835599523257">"App-handgreep"</string> <string name="app_icon_text" msgid="2823268023931811747">"App-icoon"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Volledig scherm"</string> <string name="desktop_text" msgid="1077633567027630454">"Desktopmodus"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Zwevend"</string> <string name="select_text" msgid="5139083974039906583">"Selecteren"</string> <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Openen in browser"</string> + <string name="new_window_text" msgid="6318648868380652280">"Nieuw venster"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Vensters beheren"</string> <string name="close_text" msgid="4986518933445178928">"Sluiten"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menu sluiten"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Menu openen"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Menu openen"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Scherm maximaliseren"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Scherm halveren"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Het formaat van deze app kan niet worden aangepast"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximaliseren"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Links uitlijnen"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Rechts uitlijnen"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-or/strings.xml b/libs/WindowManager/Shell/res/values-or/strings.xml index 4690c4098e13..febed9f9ffba 100644 --- a/libs/WindowManager/Shell/res/values-or/strings.xml +++ b/libs/WindowManager/Shell/res/values-or/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ଉପର-ଡାହାଣକୁ ନିଅନ୍ତୁ"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ତଳ ବାମକୁ ନିଅନ୍ତୁ"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ତଳ ଡାହାଣକୁ ନିଅନ୍ତୁ"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"ମେନୁକୁ ବିସ୍ତାର କରନ୍ତୁ"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"ମେନୁକୁ ସଙ୍କୁଚିତ କରନ୍ତୁ"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"ବାମକୁ ମୁଭ କରନ୍ତୁ"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"ଡାହାଣକୁ ମୁଭ କରନ୍ତୁ"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> ବିସ୍ତାର କରନ୍ତୁ"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> ସଙ୍କୁଚିତ କରନ୍ତୁ"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ସେଟିଂସ୍"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"ବବଲ୍ ଖାରଜ କରନ୍ତୁ"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"ପୂର୍ଣ୍ଣସ୍କ୍ରିନକୁ ମୁଭ କରନ୍ତୁ"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"ବାର୍ତ୍ତାଳାପକୁ ବବଲ୍ କରନ୍ତୁ ନାହିଁ"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"ବବଲଗୁଡ଼ିକୁ ବ୍ୟବହାର କରି ଚାଟ୍ କରନ୍ତୁ"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"ନୂଆ ବାର୍ତ୍ତାଳାପଗୁଡ଼ିକ ଫ୍ଲୋଟିଂ ଆଇକନ୍ କିମ୍ବା ବବଲ୍ ଭାବେ ଦେଖାଯିବ। ବବଲ୍ ଖୋଲିବାକୁ ଟାପ୍ କରନ୍ତୁ। ଏହାକୁ ମୁଭ୍ କରିବାକୁ ଟାଣନ୍ତୁ।"</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"ଛୋଟ କରନ୍ତୁ"</string> <string name="close_button_text" msgid="2913281996024033299">"ବନ୍ଦ କରନ୍ତୁ"</string> <string name="back_button_text" msgid="1469718707134137085">"ପଛକୁ ଫେରନ୍ତୁ"</string> - <string name="handle_text" msgid="1766582106752184456">"ହେଣ୍ଡେଲ"</string> + <string name="handle_text" msgid="4419667835599523257">"ଆପର ହେଣ୍ଡେଲ"</string> <string name="app_icon_text" msgid="2823268023931811747">"ଆପ ଆଇକନ"</string> <string name="fullscreen_text" msgid="1162316685217676079">"ପୂର୍ଣ୍ଣସ୍କ୍ରିନ"</string> <string name="desktop_text" msgid="1077633567027630454">"ଡେସ୍କଟପ ମୋଡ"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"ଫ୍ଲୋଟ"</string> <string name="select_text" msgid="5139083974039906583">"ଚୟନ କରନ୍ତୁ"</string> <string name="screenshot_text" msgid="1477704010087786671">"ସ୍କ୍ରିନସଟ"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"ବ୍ରାଉଜରରେ ଖୋଲନ୍ତୁ"</string> + <string name="new_window_text" msgid="6318648868380652280">"ନୂଆ ୱିଣ୍ଡୋ"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"ୱିଣ୍ଡୋଗୁଡ଼ିକୁ ପରିଚାଳନା କରନ୍ତୁ"</string> <string name="close_text" msgid="4986518933445178928">"ବନ୍ଦ କରନ୍ତୁ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ମେନୁ ବନ୍ଦ କରନ୍ତୁ"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"ମେନୁ ଖୋଲନ୍ତୁ"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"ମେନୁ ଖୋଲନ୍ତୁ"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ସ୍କ୍ରିନକୁ ବଡ଼ କରନ୍ତୁ"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ସ୍କ୍ରିନକୁ ସ୍ନାପ କରନ୍ତୁ"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"ଏହି ଆପକୁ ରିସାଇଜ କରାଯାଇପାରିବ ନାହିଁ"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"ବଡ଼ କରନ୍ତୁ"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ବାମରେ ସ୍ନାପ କରନ୍ତୁ"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"ଡାହାଣରେ ସ୍ନାପ କରନ୍ତୁ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml index 6a02c143ea00..e11cc1c1f76d 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ਉੱਪਰ ਵੱਲ ਸੱਜੇ ਲਿਜਾਓ"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ਹੇਠਾਂ ਵੱਲ ਖੱਬੇ ਲਿਜਾਓ"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ਹੇਠਾਂ ਵੱਲ ਸੱਜੇ ਲਿਜਾਓ"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"ਮੀਨੂ ਦਾ ਵਿਸਤਾਰ ਕਰੋ"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"ਮੀਨੂ ਨੂੰ ਸਮੇਟੋ"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"ਖੱਬੇ ਲਿਜਾਓ"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"ਸੱਜੇ ਲਿਜਾਓ"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> ਦਾ ਵਿਸਤਾਰ ਕਰੋ"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> ਨੂੰ ਸਮੇਟੋ"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ਸੈਟਿੰਗਾਂ"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"ਬਬਲ ਨੂੰ ਖਾਰਜ ਕਰੋ"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"ਪੂਰੀ-ਸਕ੍ਰੀਨ \'ਤੇ ਜਾਓ"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"ਗੱਲਬਾਤ \'ਤੇ ਬਬਲ ਨਾ ਲਾਓ"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"ਬਬਲ ਵਰਤਦੇ ਹੋਏ ਚੈਟ ਕਰੋ"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"ਨਵੀਆਂ ਗੱਲਾਂਬਾਤਾਂ ਫਲੋਟਿੰਗ ਪ੍ਰਤੀਕਾਂ ਜਾਂ ਬਬਲ ਦੇ ਰੂਪ ਵਿੱਚ ਦਿਸਦੀਆਂ ਹਨ। ਬਬਲ ਨੂੰ ਖੋਲ੍ਹਣ ਲਈ ਟੈਪ ਕਰੋ। ਇਸਨੂੰ ਲਿਜਾਣ ਲਈ ਘਸੀਟੋ।"</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"ਛੋਟਾ ਕਰੋ"</string> <string name="close_button_text" msgid="2913281996024033299">"ਬੰਦ ਕਰੋ"</string> <string name="back_button_text" msgid="1469718707134137085">"ਪਿੱਛੇ"</string> - <string name="handle_text" msgid="1766582106752184456">"ਹੈਂਡਲ"</string> + <string name="handle_text" msgid="4419667835599523257">"ਐਪ ਹੈਂਡਲ"</string> <string name="app_icon_text" msgid="2823268023931811747">"ਐਪ ਪ੍ਰਤੀਕ"</string> <string name="fullscreen_text" msgid="1162316685217676079">"ਪੂਰੀ-ਸਕ੍ਰੀਨ"</string> <string name="desktop_text" msgid="1077633567027630454">"ਡੈਸਕਟਾਪ ਮੋਡ"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"ਫ਼ਲੋਟ"</string> <string name="select_text" msgid="5139083974039906583">"ਚੁਣੋ"</string> <string name="screenshot_text" msgid="1477704010087786671">"ਸਕ੍ਰੀਨਸ਼ਾਟ"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"ਬ੍ਰਾਊਜ਼ਰ ਵਿੱਚ ਖੋਲ੍ਹੋ"</string> + <string name="new_window_text" msgid="6318648868380652280">"ਨਵੀਂ ਵਿੰਡੋ"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"ਵਿੰਡੋਆਂ ਦਾ ਪ੍ਰਬੰਧਨ ਕਰੋ"</string> <string name="close_text" msgid="4986518933445178928">"ਬੰਦ ਕਰੋ"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ਮੀਨੂ ਬੰਦ ਕਰੋ"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"ਮੀਨੂ ਖੋਲ੍ਹੋ"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"ਮੀਨੂ ਖੋਲ੍ਹੋ"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ਸਕ੍ਰੀਨ ਦਾ ਆਕਾਰ ਵਧਾਓ"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ਸਕ੍ਰੀਨ ਨੂੰ ਸਨੈਪ ਕਰੋ"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"ਇਸ ਐਪ ਦਾ ਆਕਾਰ ਬਦਲਿਆ ਨਹੀਂ ਜਾ ਸਕਦਾ"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"ਵੱਡਾ ਕਰੋ"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ਖੱਬੇ ਪਾਸੇ ਸਨੈਪ ਕਰੋ"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"ਸੱਜੇ ਪਾਸੇ ਸਨੈਪ ਕਰੋ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml index 061d45691ee0..2640c0f368a8 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Przenieś w prawy górny róg"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Przenieś w lewy dolny róg"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Przenieś w prawy dolny róg"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"rozwiń menu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"zwiń menu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Przenieś w lewo"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Przenieś w prawo"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"rozwiń dymek <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"zwiń dymek <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> – ustawienia"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Zamknij dymek"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Zmień tryb na pełnoekranowy"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nie wyświetlaj rozmowy jako dymka"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Czatuj, korzystając z dymków"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nowe rozmowy będą wyświetlane jako pływające ikony lub dymki. Kliknij, by otworzyć dymek. Przeciągnij, by go przenieść."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimalizuj"</string> <string name="close_button_text" msgid="2913281996024033299">"Zamknij"</string> <string name="back_button_text" msgid="1469718707134137085">"Wstecz"</string> - <string name="handle_text" msgid="1766582106752184456">"Uchwyt"</string> + <string name="handle_text" msgid="4419667835599523257">"Uchwyt aplikacji"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ikona aplikacji"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Pełny ekran"</string> <string name="desktop_text" msgid="1077633567027630454">"Tryb pulpitu"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Pływające"</string> <string name="select_text" msgid="5139083974039906583">"Wybierz"</string> <string name="screenshot_text" msgid="1477704010087786671">"Zrzut ekranu"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Otwórz w przeglądarce"</string> + <string name="new_window_text" msgid="6318648868380652280">"Nowe okno"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Zarządzaj oknami"</string> <string name="close_text" msgid="4986518933445178928">"Zamknij"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zamknij menu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Otwórz menu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Otwórz menu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksymalizuj ekran"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Przyciągnij ekran"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Nie można zmienić rozmiaru tej aplikacji"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksymalizuj"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Przyciągnij do lewej"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Przyciągnij do prawej"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml index d7c1b78ab238..eb3206aa7d2a 100644 --- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover para canto superior direito"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover para canto inferior esquerdo"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover para canto inferior direito"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"abrir menu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"fechar menu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Mover para a esquerda"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Mover para a direita"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"abrir <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"fechar <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Configurações de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dispensar balão"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Mude para tela cheia"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Não criar balões de conversa"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Converse usando balões"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Novas conversas aparecerão como ícones flutuantes, ou balões. Toque para abrir o balão. Arraste para movê-lo."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string> <string name="close_button_text" msgid="2913281996024033299">"Fechar"</string> <string name="back_button_text" msgid="1469718707134137085">"Voltar"</string> - <string name="handle_text" msgid="1766582106752184456">"Alça"</string> + <string name="handle_text" msgid="4419667835599523257">"Identificador do app"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ícone do app"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Tela cheia"</string> <string name="desktop_text" msgid="1077633567027630454">"Modo área de trabalho"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Ponto flutuante"</string> <string name="select_text" msgid="5139083974039906583">"Selecionar"</string> <string name="screenshot_text" msgid="1477704010087786671">"Captura de tela"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Abrir no navegador"</string> + <string name="new_window_text" msgid="6318648868380652280">"Nova janela"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Gerenciar janelas"</string> <string name="close_text" msgid="4986518933445178928">"Fechar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Abrir o menu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Abrir o menu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ampliar tela"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar tela"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Não é possível redimensionar o app"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizar"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Ajustar à esquerda"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Ajustar à direita"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml index 5e16ce9533c3..46f8b38dd621 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover p/ parte sup. direita"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover p/ parte infer. esquerda"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover p/ parte inf. direita"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"expandir menu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"reduzir menu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Mover para a esquerda"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Mover para a direita"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"expandir <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"reduzir <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Definições de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ignorar balão"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Mudar para ecrã inteiro"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Não apresentar a conversa em balões"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Converse no chat através de balões"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"As novas conversas aparecem como ícones flutuantes ou balões. Toque para abrir o balão. Arraste para o mover."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string> <string name="close_button_text" msgid="2913281996024033299">"Fechar"</string> <string name="back_button_text" msgid="1469718707134137085">"Anterior"</string> - <string name="handle_text" msgid="1766582106752184456">"Indicador"</string> + <string name="handle_text" msgid="4419667835599523257">"Indicador da app"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ícone da app"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Ecrã inteiro"</string> <string name="desktop_text" msgid="1077633567027630454">"Modo de ambiente de trabalho"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Flutuar"</string> <string name="select_text" msgid="5139083974039906583">"Selecionar"</string> <string name="screenshot_text" msgid="1477704010087786671">"Captura de ecrã"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Abrir no navegador"</string> + <string name="new_window_text" msgid="6318648868380652280">"Nova janela"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Faça a gestão das janelas"</string> <string name="close_text" msgid="4986518933445178928">"Fechar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Abrir menu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Abrir menu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar ecrã"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Encaixar ecrã"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Não é possível redimensionar esta app"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizar"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Encaixar à esquerda"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Encaixar à direita"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt/strings.xml b/libs/WindowManager/Shell/res/values-pt/strings.xml index d7c1b78ab238..eb3206aa7d2a 100644 --- a/libs/WindowManager/Shell/res/values-pt/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover para canto superior direito"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover para canto inferior esquerdo"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover para canto inferior direito"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"abrir menu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"fechar menu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Mover para a esquerda"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Mover para a direita"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"abrir <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"fechar <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Configurações de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Dispensar balão"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Mude para tela cheia"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Não criar balões de conversa"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Converse usando balões"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Novas conversas aparecerão como ícones flutuantes, ou balões. Toque para abrir o balão. Arraste para movê-lo."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string> <string name="close_button_text" msgid="2913281996024033299">"Fechar"</string> <string name="back_button_text" msgid="1469718707134137085">"Voltar"</string> - <string name="handle_text" msgid="1766582106752184456">"Alça"</string> + <string name="handle_text" msgid="4419667835599523257">"Identificador do app"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ícone do app"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Tela cheia"</string> <string name="desktop_text" msgid="1077633567027630454">"Modo área de trabalho"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Ponto flutuante"</string> <string name="select_text" msgid="5139083974039906583">"Selecionar"</string> <string name="screenshot_text" msgid="1477704010087786671">"Captura de tela"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Abrir no navegador"</string> + <string name="new_window_text" msgid="6318648868380652280">"Nova janela"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Gerenciar janelas"</string> <string name="close_text" msgid="4986518933445178928">"Fechar"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Abrir o menu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Abrir o menu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ampliar tela"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar tela"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Não é possível redimensionar o app"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizar"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Ajustar à esquerda"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Ajustar à direita"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml index 3dcbb2c48016..8b84c602112c 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mută în dreapta sus"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mută în stânga jos"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mută în dreapta jos"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"extinde meniul"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"restrânge meniul"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Deplasează spre stânga"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Deplasează spre dreapta"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"extinde <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"restrânge <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Setări <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Închide balonul"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Treci la ecran complet"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nu afișa conversația în balon"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat cu baloane"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Conversațiile noi apar ca pictograme flotante sau baloane. Atinge pentru a deschide balonul. Trage pentru a-l muta."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimizează"</string> <string name="close_button_text" msgid="2913281996024033299">"Închide"</string> <string name="back_button_text" msgid="1469718707134137085">"Înapoi"</string> - <string name="handle_text" msgid="1766582106752184456">"Ghidaj"</string> + <string name="handle_text" msgid="4419667835599523257">"Handle de aplicație"</string> <string name="app_icon_text" msgid="2823268023931811747">"Pictograma aplicației"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Ecran complet"</string> <string name="desktop_text" msgid="1077633567027630454">"Modul desktop"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Flotantă"</string> <string name="select_text" msgid="5139083974039906583">"Selectează"</string> <string name="screenshot_text" msgid="1477704010087786671">"Captură de ecran"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Deschide în browser"</string> + <string name="new_window_text" msgid="6318648868380652280">"Fereastră nouă"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Gestionează ferestrele"</string> <string name="close_text" msgid="4986518933445178928">"Închide"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Închide meniul"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Deschide meniul"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Deschide meniul"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizează fereastra"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Micșorează fereastra și fixeaz-o"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Aplicația nu poate fi redimensionată"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizează"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Trage la stânga"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Trage la dreapta"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml index 8f283866f6aa..affd0cb0853a 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Переместить в правый верхний угол"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Переместить в левый нижний угол"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Переместить в правый нижний угол"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"развернуть меню"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"свернуть меню"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Переместить влево"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Переместить вправо"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"Развернуть <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"Свернуть <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>: настройки"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Скрыть всплывающий чат"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Перейти в полноэкранный режим"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Не показывать всплывающий чат для разговора"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Всплывающие чаты"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Новые разговоры будут появляться в виде плавающих значков, или всплывающих чатов. Чтобы открыть чат, нажмите на него, а чтобы переместить – перетащите."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Свернуть"</string> <string name="close_button_text" msgid="2913281996024033299">"Закрыть"</string> <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> - <string name="handle_text" msgid="1766582106752184456">"Маркер"</string> + <string name="handle_text" msgid="4419667835599523257">"Обозначение приложения"</string> <string name="app_icon_text" msgid="2823268023931811747">"Значок приложения"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Полноэкранный режим"</string> <string name="desktop_text" msgid="1077633567027630454">"Режим компьютера"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Плавающее окно"</string> <string name="select_text" msgid="5139083974039906583">"Выбрать"</string> <string name="screenshot_text" msgid="1477704010087786671">"Скриншот"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Открыть в браузере"</string> + <string name="new_window_text" msgid="6318648868380652280">"Новое окно"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Управление окнами"</string> <string name="close_text" msgid="4986518933445178928">"Закрыть"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Закрыть меню"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Открыть меню"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Открыть меню"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Развернуть на весь экран"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Свернуть"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Изменить размер приложения нельзя."</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Развернуть"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Привязать слева"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Привязать справа"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml index 289508c1a42f..f42c62a27248 100644 --- a/libs/WindowManager/Shell/res/values-si/strings.xml +++ b/libs/WindowManager/Shell/res/values-si/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ඉහළ දකුණට ගෙන යන්න"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"පහළ වමට ගෙන යන්න"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"පහළ දකුණට ගෙන යන්න"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"මෙනුව දිග හරින්න"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"මෙනුව හකුළන්න"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"වමට ගෙන යන්න"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"දකුණට ගෙන යන්න"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> දිග හරින්න"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> හකුළන්න"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> සැකසීම්"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"බුබුලු ඉවත ලන්න"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"පූර්ණ තිරය වෙත ගෙන යන්න"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"සංවාදය බුබුලු නොදමන්න"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"බුබුලු භාවිතයෙන් කතාබහ කරන්න"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"නව සංවාද පාවෙන අයිකන හෝ බුබුලු ලෙස දිස් වේ. බුබුල විවෘත කිරීමට තට්ටු කරන්න. එය ගෙන යාමට අදින්න."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"කුඩා කරන්න"</string> <string name="close_button_text" msgid="2913281996024033299">"වසන්න"</string> <string name="back_button_text" msgid="1469718707134137085">"ආපසු"</string> - <string name="handle_text" msgid="1766582106752184456">"හැඬලය"</string> + <string name="handle_text" msgid="4419667835599523257">"යෙදුම් හසුරුව"</string> <string name="app_icon_text" msgid="2823268023931811747">"යෙදුම් නිරූපකය"</string> <string name="fullscreen_text" msgid="1162316685217676079">"පූර්ණ තිරය"</string> <string name="desktop_text" msgid="1077633567027630454">"ඩෙස්ක්ටොප් ප්රකාරය"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"පාවෙන"</string> <string name="select_text" msgid="5139083974039906583">"තෝරන්න"</string> <string name="screenshot_text" msgid="1477704010087786671">"තිර රුව"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"බ්රව්සරයේ විවෘත කරන්න"</string> + <string name="new_window_text" msgid="6318648868380652280">"නව කවුළුව"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"කවුළු කළමනාකරණය කරන්න"</string> <string name="close_text" msgid="4986518933445178928">"වසන්න"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"මෙනුව වසන්න"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"මෙනුව විවෘත කරන්න"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"මෙනුව විවෘත කරන්න"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"තිරය උපරිම කරන්න"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ස්නැප් තිරය"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"මෙම යෙදුම ප්රතිප්රමාණ කළ නොහැක"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"විහිදන්න"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"වමට ස්නැප් කරන්න"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"දකුණට ස්නැප් කරන්න"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sk/strings.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml index e4a0fe254d63..c090c6f12105 100644 --- a/libs/WindowManager/Shell/res/values-sk/strings.xml +++ b/libs/WindowManager/Shell/res/values-sk/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Presunúť doprava nahor"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Presunúť doľava nadol"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Presunúť doprava nadol"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"rozbaliť ponuku"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"zbaliť ponuku"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Posunúť doľava"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Posunúť doprava"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"rozbaliť <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"zbaliť <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Nastavenia aplikácie <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Zavrieť bublinu"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Presunúť na celú obrazovku"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Nezobrazovať konverzáciu ako bublinu"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Čet pomocou bublín"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nové konverzácie sa zobrazujú ako plávajúce ikony či bubliny. Bublinu otvoríte klepnutím. Premiestnite ju presunutím."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimalizovať"</string> <string name="close_button_text" msgid="2913281996024033299">"Zavrieť"</string> <string name="back_button_text" msgid="1469718707134137085">"Späť"</string> - <string name="handle_text" msgid="1766582106752184456">"Rukoväť"</string> + <string name="handle_text" msgid="4419667835599523257">"Rukoväť aplikácie"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ikona aplikácie"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Celá obrazovka"</string> <string name="desktop_text" msgid="1077633567027630454">"Režim počítača"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Plávajúce"</string> <string name="select_text" msgid="5139083974039906583">"Vybrať"</string> <string name="screenshot_text" msgid="1477704010087786671">"Snímka obrazovky"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Otvoriť v prehliadači"</string> + <string name="new_window_text" msgid="6318648868380652280">"Nové okno"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Správa okien"</string> <string name="close_text" msgid="4986518933445178928">"Zavrieť"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zavrieť ponuku"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Otvoriť ponuku"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Otvoriť ponuku"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximalizovať obrazovku"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Zobraziť polovicu obrazovky"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Veľkosť tejto aplikácie sa nedá zmeniť"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximalizovať"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Prichytiť vľavo"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Prichytiť vpravo"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml index e68c1cd7bf52..280346d0559a 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Premakni zgoraj desno"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Premakni spodaj levo"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Premakni spodaj desno"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"razširi meni"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"strni meni"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Premakni levo"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Premakni desno"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"razširitev oblačka <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"strnitev oblačka <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Nastavitve za <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Opusti oblaček"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Premik na celozaslonski način"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Pogovora ne prikaži v oblačku"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Klepet z oblački"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Novi pogovori so prikazani kot lebdeče ikone ali oblački. Če želite odpreti oblaček, se ga dotaknite. Če ga želite premakniti, ga povlecite."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimiraj"</string> <string name="close_button_text" msgid="2913281996024033299">"Zapri"</string> <string name="back_button_text" msgid="1469718707134137085">"Nazaj"</string> - <string name="handle_text" msgid="1766582106752184456">"Ročica"</string> + <string name="handle_text" msgid="4419667835599523257">"Identifikator aplikacije"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ikona aplikacije"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Celozaslonsko"</string> <string name="desktop_text" msgid="1077633567027630454">"Namizni način"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Lebdeče"</string> <string name="select_text" msgid="5139083974039906583">"Izberi"</string> <string name="screenshot_text" msgid="1477704010087786671">"Posnetek zaslona"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Odpri v brskalniku"</string> + <string name="new_window_text" msgid="6318648868380652280">"Novo okno"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Upravljanje oken"</string> <string name="close_text" msgid="4986518933445178928">"Zapri"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zapri meni"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Odpri meni"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Odpri meni"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimiraj zaslon"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Pripni zaslon"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Velikosti te aplikacije ni mogoče spremeniti"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimiraj"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Pripni levo"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Pripni desno"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml index a7ef2cf57a5c..bd1c41232f48 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Lëviz lart djathtas"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Zhvendos poshtë majtas"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Lëvize poshtë djathtas"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"zgjero menynë"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"palos menynë"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Lëvize majtas"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Lëvize djathtas"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"zgjero <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"palos <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Cilësimet e <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Hiqe flluskën"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Kalo në ekran të plotë"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Mos e vendos bisedën në flluskë"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Bisedo duke përdorur flluskat"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Bisedat e reja shfaqen si ikona pluskuese ose flluska. Trokit për të hapur flluskën. Zvarrit për ta zhvendosur."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimizo"</string> <string name="close_button_text" msgid="2913281996024033299">"Mbyll"</string> <string name="back_button_text" msgid="1469718707134137085">"Pas"</string> - <string name="handle_text" msgid="1766582106752184456">"Emërtimi"</string> + <string name="handle_text" msgid="4419667835599523257">"Emërtimi i aplikacionit"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ikona e aplikacionit"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Ekrani i plotë"</string> <string name="desktop_text" msgid="1077633567027630454">"Modaliteti i desktopit"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Pluskuese"</string> <string name="select_text" msgid="5139083974039906583">"Zgjidh"</string> <string name="screenshot_text" msgid="1477704010087786671">"Pamja e ekranit"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Hape në shfletues"</string> + <string name="new_window_text" msgid="6318648868380652280">"Dritare e re"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Menaxho dritaret"</string> <string name="close_text" msgid="4986518933445178928">"Mbyll"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Mbyll menynë"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Hap menynë"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Hap menynë"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimizo ekranin"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Regjistro ekranin"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Përmasat e këtij aplikacioni nuk mund të ndryshohen"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimizo"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Zhvendos majtas"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Zhvendos djathtas"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sr/strings.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml index 449c3601650f..ce9b4ae7626b 100644 --- a/libs/WindowManager/Shell/res/values-sr/strings.xml +++ b/libs/WindowManager/Shell/res/values-sr/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Премести горе десно"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Премести доле лево"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Премести доле десно"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"прошири мени"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"скупи мени"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Померите налево"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Померите надесно"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"проширите облачић <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"скупите облачић <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Подешавања за <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Одбаци облачић"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Пребаци на цео екран"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Не користи облачиће за конверзацију"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Ћаскајте у облачићима"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Нове конверзације се приказују као плутајуће иконе или облачићи. Додирните да бисте отворили облачић. Превуците да бисте га преместили."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Умањите"</string> <string name="close_button_text" msgid="2913281996024033299">"Затворите"</string> <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> - <string name="handle_text" msgid="1766582106752184456">"Идентификатор"</string> + <string name="handle_text" msgid="4419667835599523257">"Идентификатор апликације"</string> <string name="app_icon_text" msgid="2823268023931811747">"Икона апликације"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Преко целог екрана"</string> <string name="desktop_text" msgid="1077633567027630454">"Режим за рачунаре"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Плутајуће"</string> <string name="select_text" msgid="5139083974039906583">"Изаберите"</string> <string name="screenshot_text" msgid="1477704010087786671">"Снимак екрана"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Отворите у прегледачу"</string> + <string name="new_window_text" msgid="6318648868380652280">"Нови прозор"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Управљајте прозорима"</string> <string name="close_text" msgid="4986518933445178928">"Затворите"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Затворите мени"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Отворите мени"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Отворите мени"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Повећај екран"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Уклопи екран"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Величина ове апликације не може да се промени"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Увећајте"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Прикачите лево"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Прикачите десно"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml index 234365a56e8e..aa74bdef0140 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Flytta högst upp till höger"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Flytta längst ned till vänster"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Flytta längst ned till höger"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"utöka menyn"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"komprimera menyn"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Flytta åt vänster"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Flytta åt höger"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"utöka <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"komprimera <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Inställningar för <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Stäng bubbla"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Flytta till helskärm"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Visa inte konversationen i bubblor"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatta med bubblor"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Nya konversationer visas som flytande ikoner, så kallade bubblor. Tryck på bubblan om du vill öppna den. Dra den om du vill flytta den."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Minimera"</string> <string name="close_button_text" msgid="2913281996024033299">"Stäng"</string> <string name="back_button_text" msgid="1469718707134137085">"Tillbaka"</string> - <string name="handle_text" msgid="1766582106752184456">"Handtag"</string> + <string name="handle_text" msgid="4419667835599523257">"Apphandtag"</string> <string name="app_icon_text" msgid="2823268023931811747">"Appikon"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Helskärm"</string> <string name="desktop_text" msgid="1077633567027630454">"Datorläge"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Svävande"</string> <string name="select_text" msgid="5139083974039906583">"Välj"</string> <string name="screenshot_text" msgid="1477704010087786671">"Skärmbild"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Öppna i webbläsaren"</string> + <string name="new_window_text" msgid="6318648868380652280">"Nytt fönster"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Hantera fönster"</string> <string name="close_text" msgid="4986518933445178928">"Stäng"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Stäng menyn"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Öppna menyn"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Öppna menyn"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximera skärmen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fäst skärmen"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Det går inte att ändra storlek på appen"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Utöka"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Fäst till vänster"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Fäst till höger"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml index bfbe0b153e30..e622e93c941e 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Sogeza juu kulia"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Sogeza chini kushoto"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Sogeza chini kulia"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"panua menyu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"kunja menyu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Sogeza kushoto"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Sogeza kulia"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"panua <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"kunja <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Mipangilio ya <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Ondoa kiputo"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Hamishia kwenye skrini nzima"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Usiweke viputo kwenye mazungumzo"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Piga gumzo ukitumia viputo"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Mazungumzo mapya huonekena kama aikoni au viputo vinavyoelea. Gusa ili ufungue kiputo. Buruta ili ukisogeze."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Punguza"</string> <string name="close_button_text" msgid="2913281996024033299">"Funga"</string> <string name="back_button_text" msgid="1469718707134137085">"Rudi nyuma"</string> - <string name="handle_text" msgid="1766582106752184456">"Ncha"</string> + <string name="handle_text" msgid="4419667835599523257">"Utambulisho wa programu"</string> <string name="app_icon_text" msgid="2823268023931811747">"Aikoni ya Programu"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Skrini nzima"</string> <string name="desktop_text" msgid="1077633567027630454">"Hali ya Kompyuta ya mezani"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Inayoelea"</string> <string name="select_text" msgid="5139083974039906583">"Chagua"</string> <string name="screenshot_text" msgid="1477704010087786671">"Picha ya skrini"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Fungua katika kivinjari"</string> + <string name="new_window_text" msgid="6318648868380652280">"Dirisha Jipya"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Dhibiti Windows"</string> <string name="close_text" msgid="4986518933445178928">"Funga"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Funga Menyu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Fungua Menyu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Fungua Menyu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Panua Dirisha kwenye Skrini"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Panga Madirisha kwenye Skrini"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Huwezi kubadilisha ukubwa wa programu hii"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Panua"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Telezesha kushoto"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Telezesha kulia"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml index 5a0233bed1f1..a65e5945d9a0 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"மேலே வலப்புறமாக நகர்த்து"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"கீழே இடப்புறமாக நகர்த்து"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"கீழே வலதுபுறமாக நகர்த்து"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"மெனுவை விரிவாக்கு"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"மெனுவைச் சுருக்கு"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"இடப்புறம் நகர்த்து"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"வலப்புறம் நகர்த்து"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> ஐ விரிவாக்கும்"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> ஐச் சுருக்கும்"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> அமைப்புகள்"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"குமிழை அகற்று"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"முழுத்திரைக்கு மாற்று"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"உரையாடலைக் குமிழாக்காதே"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"குமிழ்களைப் பயன்படுத்தி அரட்டையடியுங்கள்"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"புதிய உரையாடல்கள் மிதக்கும் ஐகான்களாகவோ குமிழ்களாகவோ தோன்றும். குமிழைத் திறக்க தட்டவும். நகர்த்த இழுக்கவும்."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"சிறிதாக்கும்"</string> <string name="close_button_text" msgid="2913281996024033299">"மூடும்"</string> <string name="back_button_text" msgid="1469718707134137085">"பின்செல்லும்"</string> - <string name="handle_text" msgid="1766582106752184456">"ஹேண்டில்"</string> + <string name="handle_text" msgid="4419667835599523257">"ஆப்ஸ் ஹேண்டில்"</string> <string name="app_icon_text" msgid="2823268023931811747">"ஆப்ஸ் ஐகான்"</string> <string name="fullscreen_text" msgid="1162316685217676079">"முழுத்திரை"</string> <string name="desktop_text" msgid="1077633567027630454">"டெஸ்க்டாப் பயன்முறை"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"மிதக்கும் சாளரம்"</string> <string name="select_text" msgid="5139083974039906583">"தேர்ந்தெடுக்கும்"</string> <string name="screenshot_text" msgid="1477704010087786671">"ஸ்கிரீன்ஷாட்"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"உலாவியில் திறக்கும்"</string> + <string name="new_window_text" msgid="6318648868380652280">"புதிய சாளரம்"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"சாளரங்களை நிர்வகிக்கலாம்"</string> <string name="close_text" msgid="4986518933445178928">"மூடும்"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"மெனுவை மூடும்"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"மெனுவைத் திற"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"மெனுவைத் திறக்கும்"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"திரையைப் பெரிதாக்கு"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"திரையை ஸ்னாப் செய்"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"இந்த ஆப்ஸின் அளவை மாற்ற முடியாது"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"பெரிதாக்கும்"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"இடதுபுறம் நகர்த்தும்"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"வலதுபுறம் நகர்த்தும்"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml index a44ae73dbd15..1a39383cfd52 100644 --- a/libs/WindowManager/Shell/res/values-te/strings.xml +++ b/libs/WindowManager/Shell/res/values-te/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ఎగువ కుడివైపునకు జరుపు"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"దిగువ ఎడమవైపునకు తరలించు"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"దిగవు కుడివైపునకు జరుపు"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"మెనూను విస్తరించండి"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"మెనూను కుదించండి"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"ఎడమ వైపుగా జరపండి"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"కుడి వైపుగా జరపండి"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> విస్తరించండి"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>ను కుదించండి"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> సెట్టింగ్లు"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"బబుల్ను విస్మరించు"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"ఫుల్ స్క్రీన్కు వెళ్లండి"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"సంభాషణను బబుల్ చేయవద్దు"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"బబుల్స్ను ఉపయోగించి చాట్ చేయండి"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"కొత్త సంభాషణలు తేలియాడే చిహ్నాలుగా లేదా బబుల్స్ లాగా కనిపిస్తాయి. బబుల్ని తెరవడానికి నొక్కండి. తరలించడానికి లాగండి."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"కుదించండి"</string> <string name="close_button_text" msgid="2913281996024033299">"మూసివేయండి"</string> <string name="back_button_text" msgid="1469718707134137085">"వెనుకకు"</string> - <string name="handle_text" msgid="1766582106752184456">"హ్యాండిల్"</string> + <string name="handle_text" msgid="4419667835599523257">"యాప్ హ్యాండిల్"</string> <string name="app_icon_text" msgid="2823268023931811747">"యాప్ చిహ్నం"</string> <string name="fullscreen_text" msgid="1162316685217676079">"ఫుల్-స్క్రీన్"</string> <string name="desktop_text" msgid="1077633567027630454">"డెస్క్టాప్ మోడ్"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"ఫ్లోట్"</string> <string name="select_text" msgid="5139083974039906583">"ఎంచుకోండి"</string> <string name="screenshot_text" msgid="1477704010087786671">"స్క్రీన్షాట్"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"బ్రౌజర్లో తెరవండి"</string> + <string name="new_window_text" msgid="6318648868380652280">"కొత్త విండో"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"విండోలను మేనేజ్ చేయండి"</string> <string name="close_text" msgid="4986518933445178928">"మూసివేయండి"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"మెనూను మూసివేయండి"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"మెనూను తెరవండి"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"మెనూను తెరవండి"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"స్క్రీన్ సైజ్ను పెంచండి"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"స్క్రీన్ను స్నాప్ చేయండి"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"ఈ యాప్ సైజ్ను మార్చడం సాధ్యపడదు"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"మ్యాగ్జిమైజ్ చేయండి"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ఎడమ వైపున స్నాప్ చేయండి"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"కుడి వైపున స్నాప్ చేయండి"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml index ad00e465128b..9d867c385f28 100644 --- a/libs/WindowManager/Shell/res/values-th/strings.xml +++ b/libs/WindowManager/Shell/res/values-th/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ย้ายไปด้านขวาบน"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ย้ายไปด้านซ้ายล่าง"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ย้ายไปด้านขวาล่าง"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"ขยายเมนู"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"ยุบเมนู"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"ย้ายไปทางซ้าย"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"ย้ายไปทางขวา"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"ขยาย <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"ยุบ <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"การตั้งค่า <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"ปิดบับเบิล"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"เปลี่ยนเป็นแบบเต็มหน้าจอ"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"ไม่ต้องแสดงการสนทนาเป็นบับเบิล"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"แชทโดยใช้บับเบิล"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"การสนทนาใหม่ๆ จะปรากฏเป็นไอคอนแบบลอยหรือบับเบิล แตะเพื่อเปิดบับเบิล ลากเพื่อย้ายที่"</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"ย่อ"</string> <string name="close_button_text" msgid="2913281996024033299">"ปิด"</string> <string name="back_button_text" msgid="1469718707134137085">"กลับ"</string> - <string name="handle_text" msgid="1766582106752184456">"แฮนเดิล"</string> + <string name="handle_text" msgid="4419667835599523257">"แฮนเดิลแอป"</string> <string name="app_icon_text" msgid="2823268023931811747">"ไอคอนแอป"</string> <string name="fullscreen_text" msgid="1162316685217676079">"เต็มหน้าจอ"</string> <string name="desktop_text" msgid="1077633567027630454">"โหมดเดสก์ท็อป"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"ล่องลอย"</string> <string name="select_text" msgid="5139083974039906583">"เลือก"</string> <string name="screenshot_text" msgid="1477704010087786671">"ภาพหน้าจอ"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"เปิดในเบราว์เซอร์"</string> + <string name="new_window_text" msgid="6318648868380652280">"หน้าต่างใหม่"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"จัดการหน้าต่าง"</string> <string name="close_text" msgid="4986518933445178928">"ปิด"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"ปิดเมนู"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"เปิดเมนู"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"เปิดเมนู"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ขยายหน้าจอให้ใหญ่สุด"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"สแนปหน้าจอ"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"ปรับขนาดแอปนี้ไม่ได้"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"ขยายใหญ่สุด"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"จัดพอดีกับทางซ้าย"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"จัดพอดีกับทางขวา"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-tl/strings.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml index fd13626fa595..27f75da39ece 100644 --- a/libs/WindowManager/Shell/res/values-tl/strings.xml +++ b/libs/WindowManager/Shell/res/values-tl/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Ilipat sa kanan sa itaas"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Ilipat sa kaliwa sa ibaba"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Ilipat sa kanan sa ibaba"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"i-expand ang menu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"i-collapse ang menu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Ilipat pakaliwa"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Ilipat pakanan"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"I-expand ang <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"i-collapse ang <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Mga setting ng <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"I-dismiss ang bubble"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Lumipat sa fullscreen"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Huwag ipakita sa bubble ang mga pag-uusap"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Mag-chat gamit ang bubbles"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Lumalabas bilang mga nakalutang na icon o bubble ang mga bagong pag-uusap. I-tap para buksan ang bubble. I-drag para ilipat ito."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"I-minimize"</string> <string name="close_button_text" msgid="2913281996024033299">"Isara"</string> <string name="back_button_text" msgid="1469718707134137085">"Bumalik"</string> - <string name="handle_text" msgid="1766582106752184456">"Handle"</string> + <string name="handle_text" msgid="4419667835599523257">"Handle ng app"</string> <string name="app_icon_text" msgid="2823268023931811747">"Icon ng App"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Fullscreen"</string> <string name="desktop_text" msgid="1077633567027630454">"Desktop Mode"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Float"</string> <string name="select_text" msgid="5139083974039906583">"Piliin"</string> <string name="screenshot_text" msgid="1477704010087786671">"Screenshot"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Buksan sa browser"</string> + <string name="new_window_text" msgid="6318648868380652280">"Bagong Window"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Pamahalaan ang Mga Window"</string> <string name="close_text" msgid="4986518933445178928">"Isara"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Isara ang Menu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Buksan ang Menu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Buksan ang Menu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"I-maximize ang Screen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"I-snap ang Screen"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Hindi nare-resize ang app na ito"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"I-maximize"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"I-snap pakaliwa"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"I-snap pakanan"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml index a9aa82274e68..4b5a246eb89b 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Sağ üste taşı"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Sol alta taşı"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Sağ alta taşı"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"menüyü genişlet"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"menüyü daralt"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Sola taşı"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Sağa taşı"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"genişlet: <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"daralt: <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ayarları"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Baloncuğu kapat"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Tam ekrana taşı"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Görüşmeyi baloncuk olarak görüntüleme"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Baloncukları kullanarak sohbet edin"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Yeni görüşmeler kayan simgeler veya baloncuk olarak görünür. Açmak için baloncuğa dokunun. Baloncuğu taşımak için sürükleyin."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Küçült"</string> <string name="close_button_text" msgid="2913281996024033299">"Kapat"</string> <string name="back_button_text" msgid="1469718707134137085">"Geri"</string> - <string name="handle_text" msgid="1766582106752184456">"Herkese açık kullanıcı adı"</string> + <string name="handle_text" msgid="4419667835599523257">"Uygulama tanıtıcısı"</string> <string name="app_icon_text" msgid="2823268023931811747">"Uygulama Simgesi"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Tam Ekran"</string> <string name="desktop_text" msgid="1077633567027630454">"Masaüstü Modu"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Havada Süzülen"</string> <string name="select_text" msgid="5139083974039906583">"Seç"</string> <string name="screenshot_text" msgid="1477704010087786671">"Ekran görüntüsü"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Tarayıcıda aç"</string> + <string name="new_window_text" msgid="6318648868380652280">"Yeni Pencere"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Pencereleri yönet"</string> <string name="close_text" msgid="4986518933445178928">"Kapat"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menüyü kapat"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Menüyü Aç"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Menüyü aç"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ekranı Büyüt"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ekranın Yarısına Tuttur"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Bu uygulama yeniden boyutlandırılamaz"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Ekranı kapla"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Sola tuttur"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Sağa tuttur"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml index 502be067c71f..7263b8c3f551 100644 --- a/libs/WindowManager/Shell/res/values-uk/strings.xml +++ b/libs/WindowManager/Shell/res/values-uk/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Перемістити праворуч угору"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Перемістити ліворуч униз"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Перемістити праворуч униз"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"розгорнути меню"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"згорнути меню"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Перемістити ліворуч"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Перемістити праворуч"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"розгорнути \"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>\""</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"згорнути \"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>\""</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Налаштування параметра \"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>\""</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Закрити підказку"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Перейти в повноекранний режим"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Не показувати спливаючі чати для розмов"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Спливаючий чат"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Нові повідомлення чату з\'являються у вигляді спливаючих значків. Щоб відкрити чат, натисніть його, а щоб перемістити – перетягніть."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Згорнути"</string> <string name="close_button_text" msgid="2913281996024033299">"Закрити"</string> <string name="back_button_text" msgid="1469718707134137085">"Назад"</string> - <string name="handle_text" msgid="1766582106752184456">"Маркер"</string> + <string name="handle_text" msgid="4419667835599523257">"Дескриптор додатка"</string> <string name="app_icon_text" msgid="2823268023931811747">"Значок додатка"</string> <string name="fullscreen_text" msgid="1162316685217676079">"На весь екран"</string> <string name="desktop_text" msgid="1077633567027630454">"Режим комп’ютера"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Плаваюче вікно"</string> <string name="select_text" msgid="5139083974039906583">"Вибрати"</string> <string name="screenshot_text" msgid="1477704010087786671">"Знімок екрана"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Відкрити у вебпереглядачі"</string> + <string name="new_window_text" msgid="6318648868380652280">"Нове вікно"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Керувати вікнами"</string> <string name="close_text" msgid="4986518933445178928">"Закрити"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Закрити меню"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Відкрити меню"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Відкрити меню"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Розгорнути екран"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Зафіксувати екран"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Розмір вікна цього додатка не можна змінити"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Розгорнути"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Закріпити ліворуч"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Закріпити праворуч"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ur/strings.xml b/libs/WindowManager/Shell/res/values-ur/strings.xml index ea1048db2faf..78251da122c6 100644 --- a/libs/WindowManager/Shell/res/values-ur/strings.xml +++ b/libs/WindowManager/Shell/res/values-ur/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"اوپر دائیں جانب لے جائيں"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"نیچے بائیں جانب لے جائیں"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"نیچے دائیں جانب لے جائیں"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"مینو کو پھیلائیں"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"مینو کو سکیڑیں"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"بائیں منتقل کریں"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"دائیں منتقل کریں"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> کو پھیلائیں"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> کو سکیڑیں"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ترتیبات"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"بلبلہ برخاست کریں"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"مکمل اسکرین پر منتقل کریں"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"گفتگو بلبلہ نہ کریں"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"بلبلے کے ذریعے چیٹ کریں"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"نئی گفتگوئیں فلوٹنگ آئیکن یا بلبلے کے طور پر ظاہر ہوں گی۔ بلبلہ کھولنے کے لیے تھپتھپائیں۔ اسے منتقل کرنے کے لیے گھسیٹیں۔"</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"چھوٹا کریں"</string> <string name="close_button_text" msgid="2913281996024033299">"بند کریں"</string> <string name="back_button_text" msgid="1469718707134137085">"پیچھے"</string> - <string name="handle_text" msgid="1766582106752184456">"ہینڈل"</string> + <string name="handle_text" msgid="4419667835599523257">"ایپ ہینڈل"</string> <string name="app_icon_text" msgid="2823268023931811747">"ایپ کا آئیکن"</string> <string name="fullscreen_text" msgid="1162316685217676079">"مکمل اسکرین"</string> <string name="desktop_text" msgid="1077633567027630454">"ڈیسک ٹاپ موڈ"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"فلوٹ"</string> <string name="select_text" msgid="5139083974039906583">"منتخب کریں"</string> <string name="screenshot_text" msgid="1477704010087786671">"اسکرین شاٹ"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"براؤزر میں کھولیں"</string> + <string name="new_window_text" msgid="6318648868380652280">"نئی ونڈو"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Windows کا نظم کریں"</string> <string name="close_text" msgid="4986518933445178928">"بند کریں"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"مینیو بند کریں"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"مینو کھولیں"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"مینو کھولیں"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"اسکرین کو بڑا کریں"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"اسکرین کا اسناپ شاٹ لیں"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"اس ایپ کا سائز تبدیل نہیں کیا جا سکتا"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"بڑا کریں"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"دائیں منتقل کریں"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"بائیں منتقل کریں"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml index 9abfc462ef40..cd0ca82ff563 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Yuqori oʻngga surish"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Quyi chapga surish"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Quyi oʻngga surish"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"menyuni ochish"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"menyuni yopish"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Chapga siljitish"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Oʻngga siljitish"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>ni yoyish"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>ni yopish"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> sozlamalari"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Bulutchani yopish"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Butun ekranga koʻchirish"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Suhbatlar bulutchalar shaklida chiqmasin"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Bulutchalar yordamida subhatlashish"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Yangi xabarlar qalqib chiquvchi belgilar yoki bulutchalar kabi chiqadi. Xabarni ochish uchun bildirishnoma ustiga bosing. Xabarni qayta joylash uchun bildirishnomani suring."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Kichraytirish"</string> <string name="close_button_text" msgid="2913281996024033299">"Yopish"</string> <string name="back_button_text" msgid="1469718707134137085">"Orqaga"</string> - <string name="handle_text" msgid="1766582106752184456">"Identifikator"</string> + <string name="handle_text" msgid="4419667835599523257">"Ilova identifikatori"</string> <string name="app_icon_text" msgid="2823268023931811747">"Ilova belgisi"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Butun ekran"</string> <string name="desktop_text" msgid="1077633567027630454">"Desktop rejimi"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Pufakli"</string> <string name="select_text" msgid="5139083974039906583">"Tanlash"</string> <string name="screenshot_text" msgid="1477704010087786671">"Skrinshot"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Brauzerda ochish"</string> + <string name="new_window_text" msgid="6318648868380652280">"Yangi oyna"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Oynalarni boshqarish"</string> <string name="close_text" msgid="4986518933445178928">"Yopish"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Menyuni yopish"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Menyuni ochish"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Menyuni ochish"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ekranni yoyish"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ekranni biriktirish"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Bu ilova hajmini oʻzgartirish imkonsiz"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Yoyish"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Chapga tortish"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Oʻngga tortish"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml index d334c880abae..c913146b7322 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Chuyển lên trên cùng bên phải"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Chuyển tới dưới cùng bên trái"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Chuyển tới dưới cùng bên phải"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"mở rộng trình đơn"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"thu gọn trình đơn"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Di chuyển sang trái"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Di chuyển sang phải"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"mở rộng <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"thu gọn <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"Cài đặt <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Đóng bong bóng"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Chuyển sang toàn màn hình"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Dừng sử dụng bong bóng cho cuộc trò chuyện"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Trò chuyện bằng bong bóng trò chuyện"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Các cuộc trò chuyện mới sẽ xuất hiện dưới dạng biểu tượng nổi hoặc bong bóng trò chuyện. Nhấn để mở bong bóng trò chuyện. Kéo để di chuyển bong bóng trò chuyện."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Thu nhỏ"</string> <string name="close_button_text" msgid="2913281996024033299">"Đóng"</string> <string name="back_button_text" msgid="1469718707134137085">"Quay lại"</string> - <string name="handle_text" msgid="1766582106752184456">"Xử lý"</string> + <string name="handle_text" msgid="4419667835599523257">"Ô điều khiển ứng dụng"</string> <string name="app_icon_text" msgid="2823268023931811747">"Biểu tượng ứng dụng"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Toàn màn hình"</string> <string name="desktop_text" msgid="1077633567027630454">"Chế độ máy tính"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Nổi"</string> <string name="select_text" msgid="5139083974039906583">"Chọn"</string> <string name="screenshot_text" msgid="1477704010087786671">"Ảnh chụp màn hình"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Mở trong trình duyệt"</string> + <string name="new_window_text" msgid="6318648868380652280">"Cửa sổ mới"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Quản lý cửa sổ"</string> <string name="close_text" msgid="4986518933445178928">"Đóng"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Đóng trình đơn"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Mở Trình đơn"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Mở Trình đơn"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Mở rộng màn hình"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Điều chỉnh kích thước màn hình"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Không thể đổi kích thước của ứng dụng này"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Phóng to tối đa"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Di chuyển nhanh sang trái"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Di chuyển nhanh sang phải"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml index 96d82f2e5f8d..4cbc2f580593 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"移至右上角"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"移至左下角"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"移至右下角"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"展开菜单"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"收起菜单"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"左移"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"右移"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"展开“<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>”"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"收起“<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>”"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>设置"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"关闭消息气泡"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"移至全屏"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"不以消息气泡形式显示对话"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"使用消息气泡聊天"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"新对话会以浮动图标或消息气泡形式显示。点按即可打开消息气泡。拖动即可移动消息气泡。"</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"最小化"</string> <string name="close_button_text" msgid="2913281996024033299">"关闭"</string> <string name="back_button_text" msgid="1469718707134137085">"返回"</string> - <string name="handle_text" msgid="1766582106752184456">"处理"</string> + <string name="handle_text" msgid="4419667835599523257">"应用手柄"</string> <string name="app_icon_text" msgid="2823268023931811747">"应用图标"</string> <string name="fullscreen_text" msgid="1162316685217676079">"全屏"</string> <string name="desktop_text" msgid="1077633567027630454">"桌面模式"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"悬浮"</string> <string name="select_text" msgid="5139083974039906583">"选择"</string> <string name="screenshot_text" msgid="1477704010087786671">"屏幕截图"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"在浏览器中打开"</string> + <string name="new_window_text" msgid="6318648868380652280">"新窗口"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"管理窗口"</string> <string name="close_text" msgid="4986518933445178928">"关闭"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"关闭菜单"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"打开菜单"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"打开菜单"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"最大化屏幕"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"屏幕快照"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"无法调整此应用的大小"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"最大化"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"贴靠左侧"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"贴靠右侧"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml index 703cefd919cb..4bf81c66e457 100644 --- a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"移去右上角"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"移去左下角"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"移去右下角"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"展開選單"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"收合選單"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"向左移"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"向右移"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"打開<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"收埋<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"「<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>」設定"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"關閉小視窗氣泡"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"移至全螢幕"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"不要透過小視窗顯示對話"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"使用小視窗進行即時通訊"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"新對話會以浮動圖示 (小視窗) 顯示。輕按即可開啟小視窗。拖曳即可移動小視窗。"</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"最小化"</string> <string name="close_button_text" msgid="2913281996024033299">"關閉"</string> <string name="back_button_text" msgid="1469718707134137085">"返去"</string> - <string name="handle_text" msgid="1766582106752184456">"控點"</string> + <string name="handle_text" msgid="4419667835599523257">"應用程式控點"</string> <string name="app_icon_text" msgid="2823268023931811747">"應用程式圖示"</string> <string name="fullscreen_text" msgid="1162316685217676079">"全螢幕"</string> <string name="desktop_text" msgid="1077633567027630454">"桌面模式"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"浮動"</string> <string name="select_text" msgid="5139083974039906583">"選取"</string> <string name="screenshot_text" msgid="1477704010087786671">"螢幕截圖"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"在瀏覽器中開啟"</string> + <string name="new_window_text" msgid="6318648868380652280">"新視窗"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"管理視窗"</string> <string name="close_text" msgid="4986518933445178928">"關閉"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"關閉選單"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"打開選單"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"打開選單"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"畫面最大化"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"貼齊畫面"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"此應用程式無法調整大小"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"最大化"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"貼齊左邊"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"貼齊右邊"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml index abdea497c4ea..5e07a177f172 100644 --- a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"移至右上方"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"移至左下方"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"移至右下方"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"展開選單"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"收合選單"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"向左移"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"向右移"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"展開「<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>」"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"收合「<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>」"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"「<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>」設定"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"關閉對話框"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"移至全螢幕"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"不要以對話框形式顯示對話"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"透過對話框來聊天"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"新的對話會以浮動圖示或對話框形式顯示。輕觸即可開啟對話框,拖曳則可移動對話框。"</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"最小化"</string> <string name="close_button_text" msgid="2913281996024033299">"關閉"</string> <string name="back_button_text" msgid="1469718707134137085">"返回"</string> - <string name="handle_text" msgid="1766582106752184456">"控點"</string> + <string name="handle_text" msgid="4419667835599523257">"應用程式控制代碼"</string> <string name="app_icon_text" msgid="2823268023931811747">"應用程式圖示"</string> <string name="fullscreen_text" msgid="1162316685217676079">"全螢幕"</string> <string name="desktop_text" msgid="1077633567027630454">"電腦模式"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"浮動"</string> <string name="select_text" msgid="5139083974039906583">"選取"</string> <string name="screenshot_text" msgid="1477704010087786671">"螢幕截圖"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"在瀏覽器中開啟"</string> + <string name="new_window_text" msgid="6318648868380652280">"新視窗"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"管理視窗"</string> <string name="close_text" msgid="4986518933445178928">"關閉"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"關閉選單"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"開啟選單"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"開啟選單"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"畫面最大化"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"貼齊畫面"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"這個應用程式無法調整大小"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"最大化"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"靠左對齊"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"靠右對齊"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml index 10847309a00c..f73e29650fba 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings.xml @@ -65,10 +65,15 @@ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Hambisa phezulu ngakwesokudla"</string> <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Hambisa inkinobho ngakwesokunxele"</string> <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Hambisa inkinobho ngakwesokudla"</string> + <string name="bubble_accessibility_action_expand_menu" msgid="8637233525952938845">"nweba imenyu"</string> + <string name="bubble_accessibility_action_collapse_menu" msgid="2975310870146231463">"goqa imenyu"</string> + <string name="bubble_accessibility_action_move_bar_left" msgid="4803535120353716759">"Iya kwesokunxele"</string> + <string name="bubble_accessibility_action_move_bar_right" msgid="7686542531917510421">"Iya kwesokudla"</string> <string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"nweba <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"goqa <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string> <string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> izilungiselelo"</string> <string name="bubble_dismiss_text" msgid="8816558050659478158">"Cashisa ibhamuza"</string> + <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Hambisa esikrinini esigcwele"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ungayibhamuzi ingxoxo"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"Xoxa usebenzisa amabhamuza"</string> <string name="bubbles_user_education_description" msgid="4215862563054175407">"Izingxoxo ezintsha zivela njengezithonjana ezintantayo, noma amabhamuza. Thepha ukuze uvule ibhamuza. Hudula ukuze ulihambise."</string> @@ -107,7 +112,7 @@ <string name="minimize_button_text" msgid="271592547935841753">"Nciphisa"</string> <string name="close_button_text" msgid="2913281996024033299">"Vala"</string> <string name="back_button_text" msgid="1469718707134137085">"Emuva"</string> - <string name="handle_text" msgid="1766582106752184456">"Isibambo"</string> + <string name="handle_text" msgid="4419667835599523257">"Inkomba ye-App"</string> <string name="app_icon_text" msgid="2823268023931811747">"Isithonjana Se-app"</string> <string name="fullscreen_text" msgid="1162316685217676079">"Isikrini esigcwele"</string> <string name="desktop_text" msgid="1077633567027630454">"Imodi Yedeskithophu"</string> @@ -116,9 +121,16 @@ <string name="float_button_text" msgid="9221657008391364581">"Iflowuthi"</string> <string name="select_text" msgid="5139083974039906583">"Khetha"</string> <string name="screenshot_text" msgid="1477704010087786671">"Isithombe-skrini"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Vula kubhrawuza"</string> + <string name="new_window_text" msgid="6318648868380652280">"Iwindi Elisha"</string> + <string name="manage_windows_text" msgid="5567366688493093920">"Phatha Amawindi"</string> <string name="close_text" msgid="4986518933445178928">"Vala"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Vala Imenyu"</string> - <string name="expand_menu_text" msgid="3847736164494181168">"Vula Imenyu"</string> + <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Vula Imenyu"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Khulisa Isikrini Sifike Ekugcineni"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Thwebula Isikrini"</string> + <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Le app ayikwazi ukushintshwa usayizi"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Khulisa"</string> + <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Chofoza kwesobunxele"</string> + <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Chofoza kwesokudla"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml index cf18da6e7463..b7aa1581a5c1 100644 --- a/libs/WindowManager/Shell/res/values/colors.xml +++ b/libs/WindowManager/Shell/res/values/colors.xml @@ -16,7 +16,8 @@ * limitations under the License. */ --> -<resources> +<resources + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> <color name="docked_divider_handle">#ffffff</color> <color name="split_divider_background">@color/taskbar_background_dark</color> <drawable name="forced_resizable_background">#59000000</drawable> @@ -42,7 +43,7 @@ <color name="compat_controls_text">@android:color/system_neutral1_50</color> <!-- Letterbox Education --> - <color name="letterbox_education_text_secondary">@android:color/system_neutral2_200</color> + <color name="letterbox_education_text_secondary">?androidprv:attr/materialColorSecondary</color> <!-- Letterbox Dialog --> <color name="letterbox_dialog_background">@android:color/system_neutral1_900</color> diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index c2ba064ac7b6..a14461a57a95 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -17,7 +17,7 @@ <resources> <!-- Determines whether the shell features all run on another thread. This is to be overrided by the resources of the app using the Shell library. --> - <bool name="config_enableShellMainThread">false</bool> + <bool name="config_enableShellMainThread">true</bool> <!-- Determines whether to register the shell task organizer on init. TODO(b/238217847): This config is temporary until we refactor the base WMComponent. --> @@ -179,4 +179,11 @@ <!-- Whether pointer pilfer is required to start back animation. --> <bool name="config_backAnimationRequiresPointerPilfer">true</bool> + + <!-- This is to be overridden to define a list of packages mapped to web links which will be + parsed and utilized for desktop windowing's app-to-web feature. --> + <string name="generic_links_list" translatable="false"/> + + <!-- Apps that can trigger Desktop Windowing App handle Education --> + <string-array name="desktop_windowing_app_handle_education_allowlist_apps"></string-array> </resources> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 595d34664cfa..3d8718332199 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -91,12 +91,16 @@ <!-- 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_region_width">96dp</dimen> + + <dimen name="split_divider_handle_region_width">80dp</dimen> <dimen name="split_divider_handle_region_height">48dp</dimen> + <!-- The divider touch zone height is intentionally halved in portrait to avoid colliding + with the app handle.--> + <dimen name="desktop_mode_portrait_split_divider_handle_region_height">24dp</dimen> - <dimen name="split_divider_handle_width">72dp</dimen> - <dimen name="split_divider_handle_height">3dp</dimen> + <!-- Divider handle size for split screen --> + <dimen name="split_divider_handle_width">40dp</dimen> + <dimen name="split_divider_handle_height">4dp</dimen> <dimen name="split_divider_bar_width">10dp</dimen> <dimen name="split_divider_corner_size">42dp</dimen> @@ -296,38 +300,35 @@ <!-- The width of the compat hint. --> <dimen name="compat_hint_width">188dp</dimen> - <!-- The width of the camera compat hint. --> - <dimen name="camera_compat_hint_width">143dp</dimen> - <!-- The corner radius of the letterbox education dialog. --> <dimen name="letterbox_education_dialog_corner_radius">28dp</dimen> <!-- The width of the top icon in the letterbox education dialog. --> - <dimen name="letterbox_education_dialog_title_icon_width">45dp</dimen> + <dimen name="letterbox_education_dialog_title_icon_width">32dp</dimen> <!-- The height of the top icon in the letterbox education dialog. --> - <dimen name="letterbox_education_dialog_title_icon_height">44dp</dimen> + <dimen name="letterbox_education_dialog_title_icon_height">32dp</dimen> <!-- The width of an icon in the letterbox education dialog. --> - <dimen name="letterbox_education_dialog_icon_width">40dp</dimen> + <dimen name="letterbox_education_dialog_icon_width">28dp</dimen> <!-- The height of an icon in the letterbox education dialog. --> - <dimen name="letterbox_education_dialog_icon_height">32dp</dimen> + <dimen name="letterbox_education_dialog_icon_height">22dp</dimen> <!-- The fixed width of the dialog if there is enough space in the parent. --> - <dimen name="letterbox_education_dialog_width">472dp</dimen> + <dimen name="letterbox_education_dialog_width">364dp</dimen> <!-- The margin between the dialog container and its parent. --> - <dimen name="letterbox_education_dialog_margin">16dp</dimen> + <dimen name="letterbox_education_dialog_margin">24dp</dimen> <!-- The width of each action container in the letterbox education dialog --> - <dimen name="letterbox_education_dialog_action_width">140dp</dimen> + <dimen name="letterbox_education_dialog_action_width">124dp</dimen> <!-- The space between two actions in the letterbox education dialog --> - <dimen name="letterbox_education_dialog_space_between_actions">24dp</dimen> + <dimen name="letterbox_education_dialog_space_between_actions">32dp</dimen> <!-- The corner radius of the buttons in the letterbox education dialog --> - <dimen name="letterbox_education_dialog_button_radius">12dp</dimen> + <dimen name="letterbox_education_dialog_button_radius">28dp</dimen> <!-- The horizontal padding for the buttons in the letterbox education dialog --> <dimen name="letterbox_education_dialog_horizontal_padding">16dp</dimen> @@ -366,7 +367,7 @@ <dimen name="letterbox_restart_dialog_button_width">82dp</dimen> <!-- The width of the buttons in the restart dialog --> - <dimen name="letterbox_restart_dialog_button_height">36dp</dimen> + <dimen name="letterbox_restart_dialog_button_height">40dp</dimen> <!-- The corner radius of the buttons in the restart dialog --> <dimen name="letterbox_restart_dialog_button_radius">18dp</dimen> @@ -459,6 +460,11 @@ start of this area. --> <dimen name="desktop_mode_customizable_caption_margin_end">152dp</dimen> + <!-- The width of the right-aligned region that is taken up by caption elements and extra + margins when the caption has the minimize button. This will be merged with the above value + once the minimize button becomes default. --> + <dimen name="desktop_mode_customizable_caption_with_minimize_button_margin_end">204dp</dimen> + <!-- The default minimum allowed window width when resizing a window in desktop mode. --> <dimen name="desktop_mode_minimum_window_width">386dp</dimen> @@ -495,20 +501,36 @@ <!-- The radius of the Maximize menu shadow. --> <dimen name="desktop_mode_maximize_menu_shadow_radius">8dp</dimen> - <!-- The width of the handle menu in desktop mode. --> + <!-- The width of the handle menu in desktop mode. --> <dimen name="desktop_mode_handle_menu_width">216dp</dimen> + <!-- The maximum height of the handle menu in desktop mode. Three pills at 52dp each, + additional actions pill 156dp, plus 2dp spacing between them plus 4dp top padding. --> + <dimen name="desktop_mode_handle_menu_height">322dp</dimen> + + <!-- The elevation set on the handle menu pills. --> + <dimen name="desktop_mode_handle_menu_pill_elevation">1dp</dimen> + <!-- The height of the handle menu's "App Info" pill in desktop mode. --> <dimen name="desktop_mode_handle_menu_app_info_pill_height">52dp</dimen> <!-- The height of the handle menu's "Windowing" pill in desktop mode. --> <dimen name="desktop_mode_handle_menu_windowing_pill_height">52dp</dimen> - <!-- The height of the handle menu's "More Actions" pill in desktop mode. --> - <dimen name="desktop_mode_handle_menu_more_actions_pill_height">52dp</dimen> + <!-- The maximum height of the handle menu's "New Window" button in desktop mode. --> + <dimen name="desktop_mode_handle_menu_new_window_height">52dp</dimen> + + <!-- The maximum height of the handle menu's "Manage Windows" button in desktop mode. --> + <dimen name="desktop_mode_handle_menu_manage_windows_height">52dp</dimen> + + <!-- The maximum height of the handle menu's "Screenshot" button in desktop mode. --> + <dimen name="desktop_mode_handle_menu_screenshot_height">52dp</dimen> + + <!-- The height of the handle menu's "Open in browser" pill in desktop mode. --> + <dimen name="desktop_mode_handle_menu_open_in_browser_pill_height">52dp</dimen> - <!-- The height of the handle menu in desktop mode. --> - <dimen name="desktop_mode_handle_menu_height">328dp</dimen> + <!-- The margin between pills of the handle menu in desktop mode. --> + <dimen name="desktop_mode_handle_menu_pill_spacing_margin">2dp</dimen> <!-- The top margin of the handle menu in desktop mode. --> <dimen name="desktop_mode_handle_menu_margin_top">4dp</dimen> @@ -516,9 +538,6 @@ <!-- The start margin of the handle menu in desktop mode. --> <dimen name="desktop_mode_handle_menu_margin_start">6dp</dimen> - <!-- The margin between pills of the handle menu in desktop mode. --> - <dimen name="desktop_mode_handle_menu_pill_spacing_margin">2dp</dimen> - <!-- The radius of the caption menu corners. --> <dimen name="desktop_mode_handle_menu_corner_radius">26dp</dimen> @@ -531,31 +550,29 @@ <!-- The size of the icon shown in the resize veil. --> <dimen name="desktop_mode_resize_veil_icon_size">96dp</dimen> - <!-- The with of the border around the app task for edge resizing, when + <!-- The width of the border outside the app task eligible for edge resizing, when enable_windowing_edge_drag_resize is enabled. --> - <dimen name="desktop_mode_edge_handle">12dp</dimen> + <dimen name="freeform_edge_handle_outset">10dp</dimen> + + <!-- The size of the border inside the app task eligible for edge resizing, when + enable_windowing_edge_drag_resize is enabled. --> + <dimen name="freeform_edge_handle_inset">2dp</dimen> <!-- The original width of the border around the app task for edge resizing, when enable_windowing_edge_drag_resize is disabled. --> <dimen name="freeform_resize_handle">15dp</dimen> <!-- The size of the corner region for drag resizing with touch, when a larger touch region is - appropriate. Applied when enable_windowing_edge_drag_resize is enabled. --> + appropriate. --> <dimen name="desktop_mode_corner_resize_large">48dp</dimen> - <!-- The original size of the corner region for darg resizing, when - enable_windowing_edge_drag_resize is disabled. --> + <!-- The size of the corner region for drag resizing with a cursor or a stylus. --> <dimen name="freeform_resize_corner">44dp</dimen> - <!-- The width of the area at the sides of the screen where a freeform task will transition to - split select if dragged until the touch input is within the range. --> - <dimen name="desktop_mode_transition_area_width">32dp</dimen> + <!-- The thickness in dp for all desktop drag transition regions. --> + <dimen name="desktop_mode_transition_region_thickness">44dp</dimen> - <!-- The width of the area where a desktop task will transition to fullscreen. --> - <dimen name="desktop_mode_fullscreen_from_desktop_width">80dp</dimen> - - <!-- The height of the area where a desktop task will transition to fullscreen. --> - <dimen name="desktop_mode_fullscreen_from_desktop_height">40dp</dimen> + <item type="dimen" format="float" name="desktop_mode_fullscreen_region_scale">0.2</item> <!-- The height on the screen where drag to the left or right edge will result in a desktop task snapping to split size. The empty space between this and the top is to allow @@ -570,6 +587,13 @@ <!-- The vertical inset to apply to the app chip's ripple drawable --> <dimen name="desktop_mode_header_app_chip_ripple_inset_vertical">4dp</dimen> + <!-- The corner radius of the minimize button's ripple drawable --> + <dimen name="desktop_mode_header_minimize_ripple_radius">18dp</dimen> + <!-- The vertical inset to apply to the minimize button's ripple drawable --> + <dimen name="desktop_mode_header_minimize_ripple_inset_vertical">4dp</dimen> + <!-- The horizontal inset to apply to the minimize button's ripple drawable --> + <dimen name="desktop_mode_header_minimize_ripple_inset_horizontal">6dp</dimen> + <!-- The corner radius of the maximize button's ripple drawable --> <dimen name="desktop_mode_header_maximize_ripple_radius">18dp</dimen> <!-- The vertical inset to apply to the maximize button's ripple drawable --> diff --git a/libs/WindowManager/Shell/res/values/ids.xml b/libs/WindowManager/Shell/res/values/ids.xml index bc59a235517d..debcba071d9c 100644 --- a/libs/WindowManager/Shell/res/values/ids.xml +++ b/libs/WindowManager/Shell/res/values/ids.xml @@ -42,6 +42,8 @@ <item type="id" name="action_move_top_right"/> <item type="id" name="action_move_bottom_left"/> <item type="id" name="action_move_bottom_right"/> + <item type="id" name="action_move_bubble_bar_left"/> + <item type="id" name="action_move_bubble_bar_right"/> <item type="id" name="dismiss_view"/> </resources> diff --git a/libs/WindowManager/Shell/res/values/integers.xml b/libs/WindowManager/Shell/res/values/integers.xml index 583bf3341a69..300baeabae83 100644 --- a/libs/WindowManager/Shell/res/values/integers.xml +++ b/libs/WindowManager/Shell/res/values/integers.xml @@ -22,4 +22,16 @@ <integer name="bubbles_overflow_columns">4</integer> <!-- Maximum number of bubbles we allow in overflow before we dismiss the oldest one. --> <integer name="bubbles_max_overflow">16</integer> + <!-- App Handle Education - Minimum number of times an app should have been launched, in order + to be eligible to show education in it --> + <integer name="desktop_windowing_education_min_app_launch_count">3</integer> + <!-- App Handle Education - Interval at which app usage stats should be queried and updated in + cache periodically --> + <integer name="desktop_windowing_education_app_usage_cache_interval_seconds">86400</integer> + <!-- App Handle Education - Time interval in seconds for which we'll analyze app usage + stats to determine if minimum usage requirements are met. --> + <integer name="desktop_windowing_education_app_launch_interval_seconds">2592000</integer> + <!-- App Handle Education - Required time passed in seconds since device has been setup + in order to be eligible to show education --> + <integer name="desktop_windowing_education_required_time_since_setup_seconds">604800</integer> </resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 47846746b205..bda56860d3ba 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -139,6 +139,14 @@ <string name="bubble_accessibility_action_move_bottom_left">Move bottom left</string> <!-- Action in accessibility menu to move the stack of bubbles to the bottom right of the screen. [CHAR LIMIT=30]--> <string name="bubble_accessibility_action_move_bottom_right">Move bottom right</string> + <!-- Click action label for bubbles to expand menu. [CHAR LIMIT=30]--> + <string name="bubble_accessibility_action_expand_menu">expand menu</string> + <!-- Click action label for bubbles to collapse menu. [CHAR LIMIT=30]--> + <string name="bubble_accessibility_action_collapse_menu">collapse menu</string> + <!-- Action in accessibility menu to move the bubble bar to the left side of the screen. [CHAR_LIMIT=30] --> + <string name="bubble_accessibility_action_move_bar_left">Move left</string> + <!-- Action in accessibility menu to move the bubble bar to the right side of the screen. [CHAR_LIMIT=30] --> + <string name="bubble_accessibility_action_move_bar_right">Move right</string> <!-- Accessibility announcement when the stack of bubbles expands. [CHAR LIMIT=NONE]--> <string name="bubble_accessibility_announce_expand">expand <xliff:g id="bubble_title" example="Messages">%1$s</xliff:g></string> <!-- Accessibility announcement when the stack of bubbles collapses. [CHAR LIMIT=NONE]--> @@ -147,6 +155,8 @@ <string name="bubbles_app_settings"><xliff:g id="notification_title" example="Android Messages">%1$s</xliff:g> settings</string> <!-- Text used for the bubble dismiss area. Bubbles dragged to, or flung towards, this area will go away. [CHAR LIMIT=30] --> <string name="bubble_dismiss_text">Dismiss bubble</string> + <!-- Text used to move the bubble to fullscreen. [CHAR LIMIT=30] --> + <string name="bubble_fullscreen_text">Move to fullscreen</string> <!-- Button text to stop a conversation from bubbling [CHAR LIMIT=60]--> <string name="bubbles_dont_bubble_conversation">Don\u2019t bubble conversation</string> <!-- Title text for the bubbles feature education cling shown when a bubble is on screen for the first time. [CHAR LIMIT=60]--> @@ -263,7 +273,7 @@ <!-- Accessibility text for the caption back button [CHAR LIMIT=NONE] --> <string name="back_button_text">Back</string> <!-- Accessibility text for the caption handle [CHAR LIMIT=NONE] --> - <string name="handle_text">Handle</string> + <string name="handle_text">App handle</string> <!-- Accessibility text for the handle menu app icon [CHAR LIMIT=NONE] --> <string name="app_icon_text">App Icon</string> <!-- Accessibility text for the handle fullscreen button [CHAR LIMIT=NONE] --> @@ -280,14 +290,29 @@ <string name="select_text">Select</string> <!-- Accessibility text for the handle menu screenshot button [CHAR LIMIT=NONE] --> <string name="screenshot_text">Screenshot</string> + <!-- Accessibility text for the handle menu open in browser button [CHAR LIMIT=NONE] --> + <string name="open_in_browser_text">Open in browser</string> + <!-- Accessibility text for the handle menu new window button [CHAR LIMIT=NONE] --> + <string name="new_window_text">New Window</string> + <!-- Accessibility text for the handle menu new window button [CHAR LIMIT=NONE] --> + <string name="manage_windows_text">Manage Windows</string> <!-- Accessibility text for the handle menu close button [CHAR LIMIT=NONE] --> <string name="close_text">Close</string> <!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] --> <string name="collapse_menu_text">Close Menu</string> - <!-- Accessibility text for the handle menu open menu button [CHAR LIMIT=NONE] --> - <string name="expand_menu_text">Open Menu</string> + <!-- Accessibility text for the App Header's App Chip [CHAR LIMIT=NONE] --> + <string name="desktop_mode_app_header_chip_text">Open Menu</string> <!-- Maximize menu maximize button string. --> <string name="desktop_mode_maximize_menu_maximize_text">Maximize Screen</string> <!-- Maximize menu snap buttons string. --> <string name="desktop_mode_maximize_menu_snap_text">Snap Screen</string> + <!-- Snap resizing non-resizable string. --> + <string name="desktop_mode_non_resizable_snap_text">This app can\'t be resized</string> + <!-- Accessibility text for the Maximize Menu's maximize button [CHAR LIMIT=NONE] --> + <string name="desktop_mode_maximize_menu_maximize_button_text">Maximize</string> + <!-- Accessibility text for the Maximize Menu's snap left button [CHAR LIMIT=NONE] --> + <string name="desktop_mode_maximize_menu_snap_left_button_text">Snap left</string> + <!-- Accessibility text for the Maximize Menu's snap right button [CHAR LIMIT=NONE] --> + <string name="desktop_mode_maximize_menu_snap_right_button_text">Snap right</string> + </resources> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index 13c0e6646002..d061ae1ef1a7 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -82,7 +82,7 @@ <item name="android:textColor">@color/tv_pip_edu_text</item> </style> - <style name="LetterboxDialog" parent="@android:style/Theme.Holo"> + <style name="LetterboxDialog" parent="@android:style/Theme.DeviceDefault.Dialog.Alert"> <item name="android:layout_width">wrap_content</item> <item name="android:layout_height">wrap_content</item> <item name="android:background">@color/letterbox_dialog_background</item> @@ -90,7 +90,7 @@ <style name="RestartDialogTitleText"> <item name="android:textSize">24sp</item> - <item name="android:textColor">?android:attr/textColorPrimary</item> + <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item> <item name="android:lineSpacingExtra">8sp</item> <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item> </style> @@ -102,23 +102,23 @@ <style name="RestartDialogBodyText" parent="RestartDialogBodyStyle"> <item name="android:letterSpacing">0.02</item> - <item name="android:textColor">?android:attr/textColorSecondary</item> + <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item> <item name="android:lineSpacingExtra">6sp</item> </style> <style name="RestartDialogCheckboxText" parent="RestartDialogBodyStyle"> - <item name="android:textColor">?android:attr/textColorPrimary</item> + <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item> <item name="android:lineSpacingExtra">6sp</item> </style> <style name="RestartDialogDismissButton" parent="RestartDialogBodyStyle"> <item name="android:lineSpacingExtra">2sp</item> - <item name="android:textColor">?android:attr/textColorPrimary</item> + <item name="android:textColor">?androidprv:attr/materialColorPrimary</item> </style> <style name="RestartDialogConfirmButton" parent="RestartDialogBodyStyle"> <item name="android:lineSpacingExtra">2sp</item> - <item name="android:textColor">?android:attr/textColorPrimaryInverse</item> + <item name="android:textColor">?androidprv:attr/materialColorOnPrimary</item> </style> <style name="ReachabilityEduHandLayout" parent="Theme.AppCompat.Light"> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedRecentTaskInfo.aidl index 15797cdb9aba..e21bf8fb723c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.aidl +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedRecentTaskInfo.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,6 @@ * limitations under the License. */ -package com.android.wm.shell.util; +package com.android.wm.shell.shared; parcelable GroupedRecentTaskInfo;
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedRecentTaskInfo.java index c045cebdf4e0..65e079ef4f72 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedRecentTaskInfo.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.util; +package com.android.wm.shell.shared; import android.annotation.IntDef; import android.app.ActivityManager; @@ -25,8 +25,11 @@ import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.wm.shell.shared.split.SplitBounds; + import java.util.Arrays; import java.util.List; +import java.util.Set; /** * Simple container for recent tasks. May contain either a single or pair of tasks. @@ -50,6 +53,9 @@ public class GroupedRecentTaskInfo implements Parcelable { private final SplitBounds mSplitBounds; @GroupType private final int mType; + // TODO(b/348332802): move isMinimized inside each Task object instead once we have a + // replacement for RecentTaskInfo + private final int[] mMinimizedTaskIds; /** * Create new for a single task @@ -57,7 +63,7 @@ public class GroupedRecentTaskInfo implements Parcelable { public static GroupedRecentTaskInfo forSingleTask( @NonNull ActivityManager.RecentTaskInfo task) { return new GroupedRecentTaskInfo(new ActivityManager.RecentTaskInfo[]{task}, null, - TYPE_SINGLE); + TYPE_SINGLE, null /* minimizedFreeformTasks */); } /** @@ -66,28 +72,51 @@ public class GroupedRecentTaskInfo implements Parcelable { public static GroupedRecentTaskInfo forSplitTasks(@NonNull ActivityManager.RecentTaskInfo task1, @NonNull ActivityManager.RecentTaskInfo task2, @Nullable SplitBounds splitBounds) { return new GroupedRecentTaskInfo(new ActivityManager.RecentTaskInfo[]{task1, task2}, - splitBounds, TYPE_SPLIT); + splitBounds, TYPE_SPLIT, null /* minimizedFreeformTasks */); } /** * Create new for a group of freeform tasks */ public static GroupedRecentTaskInfo forFreeformTasks( - @NonNull ActivityManager.RecentTaskInfo... tasks) { - return new GroupedRecentTaskInfo(tasks, null, TYPE_FREEFORM); + @NonNull ActivityManager.RecentTaskInfo[] tasks, + @NonNull Set<Integer> minimizedFreeformTasks) { + return new GroupedRecentTaskInfo( + tasks, + null /* splitBounds */, + TYPE_FREEFORM, + minimizedFreeformTasks.stream().mapToInt(i -> i).toArray()); } - private GroupedRecentTaskInfo(@NonNull ActivityManager.RecentTaskInfo[] tasks, - @Nullable SplitBounds splitBounds, @GroupType int type) { + private GroupedRecentTaskInfo( + @NonNull ActivityManager.RecentTaskInfo[] tasks, + @Nullable SplitBounds splitBounds, + @GroupType int type, + @Nullable int[] minimizedFreeformTaskIds) { mTasks = tasks; mSplitBounds = splitBounds; mType = type; + mMinimizedTaskIds = minimizedFreeformTaskIds; + ensureAllMinimizedIdsPresent(tasks, minimizedFreeformTaskIds); + } + + private static void ensureAllMinimizedIdsPresent( + @NonNull ActivityManager.RecentTaskInfo[] tasks, + @Nullable int[] minimizedFreeformTaskIds) { + if (minimizedFreeformTaskIds == null) { + return; + } + if (!Arrays.stream(minimizedFreeformTaskIds).allMatch( + taskId -> Arrays.stream(tasks).anyMatch(task -> task.taskId == taskId))) { + throw new IllegalArgumentException("Minimized task IDs contain non-existent Task ID."); + } } GroupedRecentTaskInfo(Parcel parcel) { mTasks = parcel.createTypedArray(ActivityManager.RecentTaskInfo.CREATOR); mSplitBounds = parcel.readTypedObject(SplitBounds.CREATOR); mType = parcel.readInt(); + mMinimizedTaskIds = parcel.createIntArray(); } /** @@ -135,6 +164,10 @@ public class GroupedRecentTaskInfo implements Parcelable { return mType; } + public int[] getMinimizedTaskIds() { + return mMinimizedTaskIds; + } + @Override public String toString() { StringBuilder taskString = new StringBuilder(); @@ -161,6 +194,8 @@ public class GroupedRecentTaskInfo implements Parcelable { taskString.append("TYPE_FREEFORM"); break; } + taskString.append(", Minimized Task IDs: "); + taskString.append(Arrays.toString(mMinimizedTaskIds)); return taskString.toString(); } @@ -181,6 +216,7 @@ public class GroupedRecentTaskInfo implements Parcelable { parcel.writeTypedArray(mTasks, flags); parcel.writeTypedObject(mSplitBounds, flags); parcel.writeInt(mType); + parcel.writeIntArray(mMinimizedTaskIds); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java index c886cc999216..8f7a2e5a6789 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.sysui; +package com.android.wm.shell.shared; /** * General shell-related constants that are shared with users of the library. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TransactionPool.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransactionPool.java index 4c34566b0d98..0c5d88dc44a8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TransactionPool.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransactionPool.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common; +package com.android.wm.shell.shared; import android.util.Pools; import android.view.SurfaceControl; diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java index dc022b4afd3b..88878c6adcf2 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java @@ -25,6 +25,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; 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_PREPARE_BACK_NAVIGATION; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; @@ -39,6 +40,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManager; +import android.app.TaskInfo; import android.app.WindowConfiguration; import android.graphics.Rect; import android.util.ArrayMap; @@ -59,7 +61,8 @@ public class TransitionUtil { public static boolean isOpeningType(@WindowManager.TransitionType int type) { return type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT - || type == TRANSIT_KEYGUARD_GOING_AWAY; + || type == TRANSIT_KEYGUARD_GOING_AWAY + || type == TRANSIT_PREPARE_BACK_NAVIGATION; } /** @return true if the transition was triggered by closing something vs opening something */ @@ -337,6 +340,52 @@ public class TransitionUtil { return target; } + /** + * Creates a new RemoteAnimationTarget from the provided change and leash + */ + public static RemoteAnimationTarget newSyntheticTarget(ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl leash, @TransitionInfo.TransitionMode int mode, int order, + boolean isTranslucent) { + int taskId; + boolean isNotInRecents; + WindowConfiguration windowConfiguration; + + if (taskInfo != null) { + taskId = taskInfo.taskId; + isNotInRecents = !taskInfo.isRunning; + windowConfiguration = taskInfo.configuration.windowConfiguration; + } else { + taskId = INVALID_TASK_ID; + isNotInRecents = true; + windowConfiguration = new WindowConfiguration(); + } + + Rect localBounds = new Rect(); + RemoteAnimationTarget target = new RemoteAnimationTarget( + taskId, + newModeToLegacyMode(mode), + // TODO: once we can properly sync transactions across process, + // then get rid of this leash. + leash, + isTranslucent, + null, + // TODO(shell-transitions): we need to send content insets? evaluate how its used. + new Rect(0, 0, 0, 0), + order, + null, + localBounds, + new Rect(), + windowConfiguration, + isNotInRecents, + null, + new Rect(), + taskInfo, + false, + INVALID_WINDOW_TYPE + ); + return target; + } + private static RemoteAnimationTarget getDividerTarget(TransitionInfo.Change change, SurfaceControl leash) { return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TriangleShape.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TriangleShape.java index 707919033065..0ca53278acbb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TriangleShape.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TriangleShape.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common; +package com.android.wm.shell.shared; import android.graphics.Outline; import android.graphics.Path; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java index ce0bf8b29374..f45dc3a1e892 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.animation; +package com.android.wm.shell.shared.animation; import android.graphics.Path; import android.view.animation.BackGestureInterpolator; diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt index 235b9bf7b9fd..fc3dc1465dff 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt @@ -168,6 +168,16 @@ object PhysicsAnimatorTestUtils { } } + /** Whether any animation is currently running. */ + @JvmStatic + fun isAnyAnimationRunning(): Boolean { + for (target in allAnimatedObjects) { + val animator = PhysicsAnimator.getInstance(target) + if (animator.isRunning()) return true + } + return false + } + /** * Blocks the calling thread until the first animation frame in which predicate returns true. If * the given object isn't animating, returns without blocking. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BaseBubblePinController.kt index eec24683db8a..7086691e7431 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BaseBubblePinController.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles +package com.android.wm.shell.shared.bubbles import android.graphics.Point import android.graphics.RectF @@ -23,9 +23,9 @@ import androidx.annotation.VisibleForTesting import androidx.core.animation.Animator import androidx.core.animation.AnimatorListenerAdapter import androidx.core.animation.ObjectAnimator -import com.android.wm.shell.common.bubbles.BaseBubblePinController.LocationChangeListener -import com.android.wm.shell.common.bubbles.BubbleBarLocation.LEFT -import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT +import com.android.wm.shell.shared.bubbles.BaseBubblePinController.LocationChangeListener +import com.android.wm.shell.shared.bubbles.BubbleBarLocation.LEFT +import com.android.wm.shell.shared.bubbles.BubbleBarLocation.RIGHT /** * Base class for common logic shared between different bubble views to support pinning bubble bar diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.aidl index 3c5beeb48806..4fe76115fa0b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.aidl +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.aidl @@ -14,6 +14,6 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles; +package com.android.wm.shell.shared.bubbles; parcelable BubbleBarLocation;
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt index f0bdfdef1073..191875d38daf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.wm.shell.common.bubbles +package com.android.wm.shell.shared.bubbles import android.os.Parcel import android.os.Parcelable diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarUpdate.java index ec3c6013e544..5bde1e8fae3b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarUpdate.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles; +package com.android.wm.shell.shared.bubbles; import android.annotation.NonNull; import android.annotation.Nullable; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleConstants.java index 0329b8df7544..3396bc441467 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleConstants.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleConstants.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles; +package com.android.wm.shell.shared.bubbles; /** * Constants shared between bubbles in shell & things we have to do for bubbles in launcher. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleInfo.java index 829af08e612a..58766826bd3b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleInfo.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles; +package com.android.wm.shell.shared.bubbles; import android.annotation.NonNull; import android.annotation.Nullable; @@ -48,10 +48,11 @@ public class BubbleInfo implements Parcelable { @Nullable private String mAppName; private boolean mIsImportantConversation; + private boolean mShowAppBadge; public BubbleInfo(String key, int flags, @Nullable String shortcutId, @Nullable Icon icon, int userId, String packageName, @Nullable String title, @Nullable String appName, - boolean isImportantConversation) { + boolean isImportantConversation, boolean showAppBadge) { mKey = key; mFlags = flags; mShortcutId = shortcutId; @@ -61,6 +62,7 @@ public class BubbleInfo implements Parcelable { mTitle = title; mAppName = appName; mIsImportantConversation = isImportantConversation; + mShowAppBadge = showAppBadge; } private BubbleInfo(Parcel source) { @@ -73,6 +75,7 @@ public class BubbleInfo implements Parcelable { mTitle = source.readString(); mAppName = source.readString(); mIsImportantConversation = source.readBoolean(); + mShowAppBadge = source.readBoolean(); } public String getKey() { @@ -115,6 +118,10 @@ public class BubbleInfo implements Parcelable { return mIsImportantConversation; } + public boolean showAppBadge() { + return mShowAppBadge; + } + /** * Whether this bubble is currently being hidden from the stack. */ @@ -172,6 +179,7 @@ public class BubbleInfo implements Parcelable { parcel.writeString(mTitle); parcel.writeString(mAppName); parcel.writeBoolean(mIsImportantConversation); + parcel.writeBoolean(mShowAppBadge); } @NonNull diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupDrawable.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubblePopupDrawable.kt index 887af17c9653..8681acf93ab3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupDrawable.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubblePopupDrawable.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.wm.shell.common.bubbles +package com.android.wm.shell.shared.bubbles import android.annotation.ColorInt import android.graphics.Canvas diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupView.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubblePopupView.kt index 444fbf7884be..802d7d131d95 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupView.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubblePopupView.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.wm.shell.common.bubbles +package com.android.wm.shell.shared.bubbles import android.content.Context import android.graphics.Rect diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissCircleView.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DismissCircleView.java index 7c5bb211a4cc..0c051560f714 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissCircleView.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DismissCircleView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles; +package com.android.wm.shell.shared.bubbles; import android.content.Context; import android.content.res.Configuration; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissView.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DismissView.kt index e06de9e9353c..2bb66b0bbcd3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissView.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DismissView.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles +package com.android.wm.shell.shared.bubbles import android.animation.ObjectAnimator import android.content.Context diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/OWNERS index 08c70314973e..08c70314973e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/OWNERS diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/RelativeTouchListener.kt index 4e55ba23407b..b1f4e331a98d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/RelativeTouchListener.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles +package com.android.wm.shell.shared.bubbles import android.graphics.PointF import android.view.MotionEvent diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/RemovedBubble.java index f90591b84b7e..c83696c01613 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/RemovedBubble.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles; +package com.android.wm.shell.shared.bubbles; import android.annotation.NonNull; import android.os.Parcel; diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index 4876f327a650..647a555ad169 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java @@ -14,21 +14,27 @@ * limitations under the License. */ -package com.android.wm.shell.shared; +package com.android.wm.shell.shared.desktopmode; import android.annotation.NonNull; import android.content.Context; import android.os.SystemProperties; +import android.window.flags.DesktopModeFlags; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.window.flags.Flags; +import java.io.PrintWriter; + /** * Constants for desktop mode feature */ +// TODO(b/237575897): Move this file to the `com.android.wm.shell.shared.desktopmode` package public class DesktopModeStatus { + private static final String TAG = "DesktopModeStatus"; + /** * Flag to indicate whether task resizing is veiled. */ @@ -67,6 +73,10 @@ public class DesktopModeStatus { private static final boolean ENFORCE_DEVICE_RESTRICTIONS = SystemProperties.getBoolean( "persist.wm.debug.desktop_mode_enforce_device_restrictions", true); + private static final boolean USE_APP_TO_WEB_BUILD_TIME_GENERIC_LINKS = + SystemProperties.getBoolean( + "persist.wm.debug.use_app_to_web_build_time_generic_links", true); + /** Whether the desktop density override is enabled. */ public static final boolean DESKTOP_DENSITY_OVERRIDE_ENABLED = SystemProperties.getBoolean("persist.wm.debug.desktop_mode_density_enabled", false); @@ -82,29 +92,25 @@ public class DesktopModeStatus { private static final int DESKTOP_DENSITY_MAX = 1000; /** - * Default value for {@code MAX_TASK_LIMIT}. + * Sysprop declaring whether to enters desktop mode by default when the windowing mode of the + * display's root TaskDisplayArea is set to WINDOWING_MODE_FREEFORM. + * + * <p>If it is not defined, then {@code R.integer.config_enterDesktopByDefaultOnFreeformDisplay} + * is used. */ - @VisibleForTesting - public static final int DEFAULT_MAX_TASK_LIMIT = 4; + public static final String ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP = + "persist.wm.debug.enter_desktop_by_default_on_freeform_display"; - // TODO(b/335131008): add a config-overlay field for the max number of tasks in Desktop Mode /** - * Flag declaring the maximum number of Tasks to show in Desktop Mode at any one time. + * Sysprop declaring the maximum number of Tasks to show in Desktop Mode at any one time. * - * <p> The limit does NOT affect Picture-in-Picture, Bubbles, or System Modals (like a screen + * <p>If it is not defined, then {@code R.integer.config_maxDesktopWindowingActiveTasks} is + * used. + * + * <p>The limit does NOT affect Picture-in-Picture, Bubbles, or System Modals (like a screen * recording window, or Bluetooth pairing window). */ - private static final int MAX_TASK_LIMIT = SystemProperties.getInt( - "persist.wm.debug.desktop_max_task_limit", DEFAULT_MAX_TASK_LIMIT); - - /** - * Return {@code true} if desktop windowing is enabled. Only to be used for testing. Callers - * should use {@link #canEnterDesktopMode(Context)} to query the state of desktop windowing. - */ - @VisibleForTesting - public static boolean isEnabled() { - return Flags.enableDesktopWindowingMode(); - } + private static final String MAX_TASK_LIMIT_SYS_PROP = "persist.wm.debug.desktop_max_task_limit"; /** * Return {@code true} if veiled resizing is active. If false, fluid resizing is used. @@ -120,7 +126,7 @@ public class DesktopModeStatus { */ public static boolean useWindowShadow(boolean isFocusedWindow) { return USE_WINDOW_SHADOWS - || (USE_WINDOW_SHADOWS_FOCUSED_WINDOW && isFocusedWindow); + || (USE_WINDOW_SHADOWS_FOCUSED_WINDOW && isFocusedWindow); } /** @@ -141,8 +147,9 @@ public class DesktopModeStatus { /** * Return the maximum limit on the number of Tasks to show in Desktop Mode at any one time. */ - public static int getMaxTaskLimit() { - return MAX_TASK_LIMIT; + public static int getMaxTaskLimit(@NonNull Context context) { + return SystemProperties.getInt(MAX_TASK_LIMIT_SYS_PROP, + context.getResources().getInteger(R.integer.config_maxDesktopWindowingActiveTasks)); } /** @@ -154,10 +161,24 @@ public class DesktopModeStatus { } /** + * Return {@code true} if desktop mode dev option should be shown on current device + */ + public static boolean canShowDesktopModeDevOption(@NonNull Context context) { + return isDeviceEligibleForDesktopMode(context) && Flags.showDesktopWindowingDevOption(); + } + + /** Returns if desktop mode dev option should be enabled if there is no user override. */ + public static boolean shouldDevOptionBeEnabledByDefault() { + return Flags.enableDesktopWindowingMode(); + } + + /** * Return {@code true} if desktop mode is enabled and can be entered on the current device. */ public static boolean canEnterDesktopMode(@NonNull Context context) { - return (!enforceDeviceRestrictions() || isDesktopModeSupported(context)) && isEnabled(); + if (!isDeviceEligibleForDesktopMode(context)) return false; + + return DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue(); } /** @@ -168,6 +189,13 @@ public class DesktopModeStatus { } /** + * Returns {@code true} if the app-to-web feature is using the build-time generic links list. + */ + public static boolean useAppToWebBuildTimeGenericLinks() { + return USE_APP_TO_WEB_BUILD_TIME_GENERIC_LINKS; + } + + /** * Return {@code true} if the override desktop density is enabled. */ private static boolean isDesktopDensityOverrideEnabled() { @@ -181,4 +209,39 @@ public class DesktopModeStatus { return DESKTOP_DENSITY_OVERRIDE >= DESKTOP_DENSITY_MIN && DESKTOP_DENSITY_OVERRIDE <= DESKTOP_DENSITY_MAX; } + + /** + * Return {@code true} if desktop mode is unrestricted and is supported in the device. + */ + private static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) { + return !enforceDeviceRestrictions() || isDesktopModeSupported(context); + } + + /** + * Return {@code true} if a display should enter desktop mode by default when the windowing mode + * of the display's root [TaskDisplayArea] is set to WINDOWING_MODE_FREEFORM. + */ + public static boolean enterDesktopByDefaultOnFreeformDisplay(@NonNull Context context) { + if (!Flags.enterDesktopByDefaultOnFreeformDisplays()) { + return false; + } + return SystemProperties.getBoolean(ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP, + context.getResources().getBoolean( + R.bool.config_enterDesktopByDefaultOnFreeformDisplay)); + } + + /** Dumps DesktopModeStatus flags and configs. */ + public static void dump(PrintWriter pw, String prefix, Context context) { + String innerPrefix = prefix + " "; + pw.print(prefix); pw.println(TAG); + pw.print(innerPrefix); pw.print("maxTaskLimit="); pw.println(getMaxTaskLimit(context)); + + pw.print(innerPrefix); pw.print("maxTaskLimit config override="); + pw.println(context.getResources().getInteger( + R.integer.config_maxDesktopWindowingActiveTasks)); + + SystemProperties.Handle maxTaskLimitHandle = SystemProperties.find(MAX_TASK_LIMIT_SYS_PROP); + pw.print(innerPrefix); pw.print("maxTaskLimit sysprop="); + pw.println(maxTaskLimitHandle == null ? "null" : maxTaskLimitHandle.getInt(/* def= */ -1)); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.aidl index c968e809bf61..f7ddf71245e1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.aidl +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.aidl @@ -14,6 +14,6 @@ * limitations under the License. */ -package com.android.wm.shell.common.desktopmode; +package com.android.wm.shell.shared.desktopmode; parcelable DesktopModeTransitionSource;
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt index dbbf178613b5..d15fbed409b8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.desktopmode +package com.android.wm.shell.shared.desktopmode import android.os.Parcel import android.os.Parcelable diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/ManageWindowsViewContainer.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/ManageWindowsViewContainer.kt new file mode 100644 index 000000000000..79becb0a2e20 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/ManageWindowsViewContainer.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2024 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.shared.desktopmode +import android.annotation.ColorInt +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.RoundRectShape +import android.util.TypedValue +import android.view.MotionEvent.ACTION_OUTSIDE +import android.view.SurfaceView +import android.view.ViewGroup.MarginLayoutParams +import android.widget.LinearLayout +import android.window.TaskSnapshot + +/** + * View for the All Windows menu option, used by both Desktop Windowing and Taskbar. + * The menu displays icons of all open instances of an app. Clicking the icon should launch + * the instance, which will be performed by the child class. + */ +abstract class ManageWindowsViewContainer( + val context: Context, + @ColorInt private val menuBackgroundColor: Int +) { + lateinit var menuView: ManageWindowsView + + /** Creates the base menu view and fills it with icon views. */ + fun show(snapshotList: List<Pair<Int, TaskSnapshot>>, + onIconClickListener: ((Int) -> Unit), + onOutsideClickListener: (() -> Unit)): ManageWindowsView { + menuView = ManageWindowsView(context, menuBackgroundColor).apply { + this.onOutsideClickListener = onOutsideClickListener + this.onIconClickListener = onIconClickListener + this.generateIconViews(snapshotList) + } + addToContainer(menuView) + return menuView + } + + /** Adds the menu view to the container responsible for displaying it. */ + abstract fun addToContainer(menuView: ManageWindowsView) + + /** Dispose of the menu, perform needed cleanup. */ + abstract fun close() + + companion object { + const val MANAGE_WINDOWS_MINIMUM_INSTANCES = 2 + } + + class ManageWindowsView( + private val context: Context, + menuBackgroundColor: Int + ) { + val rootView: LinearLayout = LinearLayout(context) + var menuHeight = 0 + var menuWidth = 0 + var onIconClickListener: ((Int) -> Unit)? = null + var onOutsideClickListener: (() -> Unit)? = null + + init { + rootView.orientation = LinearLayout.VERTICAL + val menuBackground = ShapeDrawable() + val menuRadius = getDimensionPixelSize(MENU_RADIUS_DP) + menuBackground.shape = RoundRectShape( + FloatArray(8) { menuRadius }, + null, + null + ) + menuBackground.paint.color = menuBackgroundColor + rootView.background = menuBackground + rootView.elevation = getDimensionPixelSize(MENU_ELEVATION_DP) + rootView.setOnTouchListener { _, event -> + if (event.actionMasked == ACTION_OUTSIDE) { + onOutsideClickListener?.invoke() + } + return@setOnTouchListener true + } + } + + private fun getDimensionPixelSize(sizeDp: Float): Float { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + sizeDp, context.resources.displayMetrics) + } + + fun generateIconViews( + snapshotList: List<Pair<Int, TaskSnapshot>> + ) { + menuWidth = 0 + menuHeight = 0 + rootView.removeAllViews() + val instanceIconHeight = getDimensionPixelSize(ICON_HEIGHT_DP) + val instanceIconWidth = getDimensionPixelSize(ICON_WIDTH_DP) + val iconRadius = getDimensionPixelSize(ICON_RADIUS_DP) + val iconMargin = getDimensionPixelSize(ICON_MARGIN_DP) + var rowLayout: LinearLayout? = null + // Add each icon to the menu, adding a new row when needed. + for ((iconCount, taskInfoSnapshotPair) in snapshotList.withIndex()) { + val taskId = taskInfoSnapshotPair.first + val snapshot = taskInfoSnapshotPair.second + // Once a row is filled, make a new row and increase the menu height. + if (iconCount % MENU_MAX_ICONS_PER_ROW == 0) { + rowLayout = LinearLayout(context) + rowLayout.orientation = LinearLayout.HORIZONTAL + rootView.addView(rowLayout) + menuHeight += (instanceIconHeight + iconMargin).toInt() + } + val snapshotBitmap = Bitmap.wrapHardwareBuffer( + snapshot.hardwareBuffer, + snapshot.colorSpace + ) + val scaledSnapshotBitmap = snapshotBitmap?.let { + Bitmap.createScaledBitmap( + it, instanceIconWidth.toInt(), instanceIconHeight.toInt(), true /* filter */ + ) + } + val appSnapshotButton = SurfaceView(context) + appSnapshotButton.cornerRadius = iconRadius + appSnapshotButton.setZOrderOnTop(true) + appSnapshotButton.setOnClickListener { + onIconClickListener?.invoke(taskId) + } + val lp = MarginLayoutParams( + instanceIconWidth.toInt(), instanceIconHeight.toInt() + ) + lp.apply { + marginStart = iconMargin.toInt() + topMargin = iconMargin.toInt() + } + appSnapshotButton.layoutParams = lp + // If we haven't already reached one full row, increment width. + if (iconCount < MENU_MAX_ICONS_PER_ROW) { + menuWidth += (instanceIconWidth + iconMargin).toInt() + } + rowLayout?.addView(appSnapshotButton) + appSnapshotButton.requestLayout() + rowLayout?.post { + appSnapshotButton.holder.surface + .attachAndQueueBufferWithColorSpace( + scaledSnapshotBitmap?.hardwareBuffer, + scaledSnapshotBitmap?.colorSpace + ) + } + } + // Add margin again for the right/bottom of the menu. + menuWidth += iconMargin.toInt() + menuHeight += iconMargin.toInt() + } + + companion object { + private const val MENU_RADIUS_DP = 26f + private const val ICON_WIDTH_DP = 204f + private const val ICON_HEIGHT_DP = 127.5f + private const val ICON_RADIUS_DP = 16f + private const val ICON_MARGIN_DP = 16f + private const val MENU_ELEVATION_DP = 1f + private const val MENU_MAX_ICONS_PER_ROW = 3 + } + } +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/OWNERS b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/OWNERS new file mode 100644 index 000000000000..2fabd4a33586 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/OWNERS @@ -0,0 +1 @@ +file:/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java index 20da54efd286..4127adc1f901 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropConstants.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.draganddrop; +package com.android.wm.shell.shared.draganddrop; /** Constants that can be used by both Shell and other users of the library, e.g. Launcher */ public class DragAndDropConstants { diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/handles/RegionSamplingHelper.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/handles/RegionSamplingHelper.java new file mode 100644 index 000000000000..a06cf78d0898 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/handles/RegionSamplingHelper.java @@ -0,0 +1,359 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.handles; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.annotation.TargetApi; +import android.graphics.Rect; +import android.os.Build; +import android.os.Handler; +import android.view.CompositionSamplingListener; +import android.view.SurfaceControl; +import android.view.View; +import android.view.ViewRootImpl; +import android.view.ViewTreeObserver; + +import androidx.annotation.VisibleForTesting; + +import java.io.PrintWriter; +import java.util.concurrent.Executor; + +/** + * A helper class to sample regions on the screen and inspect its luminosity. + */ +@TargetApi(Build.VERSION_CODES.Q) +public class RegionSamplingHelper implements View.OnAttachStateChangeListener, + View.OnLayoutChangeListener { + + // Luminance threshold to determine black/white contrast for the navigation affordances. + // Passing the threshold of this luminance value will make the button black otherwise white + private static final float NAVIGATION_LUMINANCE_THRESHOLD = 0.5f; + // Luminance change threshold that allows applying new value if difference was exceeded + private static final float NAVIGATION_LUMINANCE_CHANGE_THRESHOLD = 0.05f; + + private final Handler mHandler = new Handler(); + private final View mSampledView; + + private final CompositionSamplingListener mSamplingListener; + + /** + * The requested sampling bounds that we want to sample from + */ + private final Rect mSamplingRequestBounds = new Rect(); + + /** + * The sampling bounds that are currently registered. + */ + private final Rect mRegisteredSamplingBounds = new Rect(); + private final SamplingCallback mCallback; + private final Executor mBackgroundExecutor; + private final SysuiCompositionSamplingListener mCompositionSamplingListener; + private boolean mSamplingEnabled = false; + private boolean mSamplingListenerRegistered = false; + + private float mLastMedianLuma; + private float mCurrentMedianLuma; + private boolean mWaitingOnDraw; + private boolean mIsDestroyed; + + private boolean mFirstSamplingAfterStart; + private boolean mWindowVisible; + private boolean mWindowHasBlurs; + private SurfaceControl mRegisteredStopLayer = null; + // A copy of mRegisteredStopLayer where we own the life cycle and can access from a bg thread. + private SurfaceControl mWrappedStopLayer = null; + private ViewTreeObserver.OnDrawListener mUpdateOnDraw = new ViewTreeObserver.OnDrawListener() { + @Override + public void onDraw() { + // We need to post the remove runnable, since it's not allowed to remove in onDraw + mHandler.post(mRemoveDrawRunnable); + RegionSamplingHelper.this.onDraw(); + } + }; + private Runnable mRemoveDrawRunnable = new Runnable() { + @Override + public void run() { + mSampledView.getViewTreeObserver().removeOnDrawListener(mUpdateOnDraw); + } + }; + + /** + * @deprecated Pass a main executor. + */ + public RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback, + Executor backgroundExecutor) { + this(sampledView, samplingCallback, sampledView.getContext().getMainExecutor(), + backgroundExecutor); + } + + public RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback, + Executor mainExecutor, Executor backgroundExecutor) { + this(sampledView, samplingCallback, mainExecutor, + backgroundExecutor, new SysuiCompositionSamplingListener()); + } + + @VisibleForTesting + RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback, + Executor mainExecutor, Executor backgroundExecutor, + SysuiCompositionSamplingListener compositionSamplingListener) { + mBackgroundExecutor = backgroundExecutor; + mCompositionSamplingListener = compositionSamplingListener; + mSamplingListener = new CompositionSamplingListener(mainExecutor) { + @Override + public void onSampleCollected(float medianLuma) { + if (mSamplingEnabled) { + updateMedianLuma(medianLuma); + } + } + }; + mSampledView = sampledView; + mSampledView.addOnAttachStateChangeListener(this); + mSampledView.addOnLayoutChangeListener(this); + + mCallback = samplingCallback; + } + + /** + * Make callback accessible + */ + @VisibleForTesting + public SamplingCallback getCallback() { + return mCallback; + } + + private void onDraw() { + if (mWaitingOnDraw) { + mWaitingOnDraw = false; + updateSamplingListener(); + } + } + + public void start(Rect initialSamplingBounds) { + if (!mCallback.isSamplingEnabled()) { + return; + } + if (initialSamplingBounds != null) { + mSamplingRequestBounds.set(initialSamplingBounds); + } + mSamplingEnabled = true; + // make sure we notify once + mLastMedianLuma = -1; + mFirstSamplingAfterStart = true; + updateSamplingListener(); + } + + public void stop() { + mSamplingEnabled = false; + updateSamplingListener(); + } + + public void stopAndDestroy() { + stop(); + mBackgroundExecutor.execute(mSamplingListener::destroy); + mIsDestroyed = true; + } + + @Override + public void onViewAttachedToWindow(View view) { + updateSamplingListener(); + } + + @Override + public void onViewDetachedFromWindow(View view) { + stopAndDestroy(); + } + + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + updateSamplingRect(); + } + + private void updateSamplingListener() { + boolean isSamplingEnabled = mSamplingEnabled + && !mSamplingRequestBounds.isEmpty() + && mWindowVisible + && !mWindowHasBlurs + && (mSampledView.isAttachedToWindow() || mFirstSamplingAfterStart); + if (isSamplingEnabled) { + ViewRootImpl viewRootImpl = mSampledView.getViewRootImpl(); + SurfaceControl stopLayerControl = null; + if (viewRootImpl != null) { + stopLayerControl = viewRootImpl.getSurfaceControl(); + } + if (stopLayerControl == null || !stopLayerControl.isValid()) { + if (!mWaitingOnDraw) { + mWaitingOnDraw = true; + // The view might be attached but we haven't drawn yet, so wait until the + // next draw to update the listener again with the stop layer, such that our + // own drawing doesn't affect the sampling. + if (mHandler.hasCallbacks(mRemoveDrawRunnable)) { + mHandler.removeCallbacks(mRemoveDrawRunnable); + } else { + mSampledView.getViewTreeObserver().addOnDrawListener(mUpdateOnDraw); + } + } + // If there's no valid surface, let's just sample without a stop layer, so we + // don't have to delay + stopLayerControl = null; + } + if (!mSamplingRequestBounds.equals(mRegisteredSamplingBounds) + || mRegisteredStopLayer != stopLayerControl) { + // We only want to re-register if something actually changed + unregisterSamplingListener(); + mSamplingListenerRegistered = true; + SurfaceControl wrappedStopLayer = wrap(stopLayerControl); + + // pass this to background thread to avoid empty Rect race condition + final Rect boundsCopy = new Rect(mSamplingRequestBounds); + + mBackgroundExecutor.execute(() -> { + if (wrappedStopLayer != null && !wrappedStopLayer.isValid()) { + return; + } + mCompositionSamplingListener.register(mSamplingListener, DEFAULT_DISPLAY, + wrappedStopLayer, boundsCopy); + }); + mRegisteredSamplingBounds.set(mSamplingRequestBounds); + mRegisteredStopLayer = stopLayerControl; + mWrappedStopLayer = wrappedStopLayer; + } + mFirstSamplingAfterStart = false; + } else { + unregisterSamplingListener(); + } + } + + @VisibleForTesting + protected SurfaceControl wrap(SurfaceControl stopLayerControl) { + return stopLayerControl == null ? null : new SurfaceControl(stopLayerControl, + "regionSampling"); + } + + private void unregisterSamplingListener() { + if (mSamplingListenerRegistered) { + mSamplingListenerRegistered = false; + SurfaceControl wrappedStopLayer = mWrappedStopLayer; + mRegisteredStopLayer = null; + mWrappedStopLayer = null; + mRegisteredSamplingBounds.setEmpty(); + mBackgroundExecutor.execute(() -> { + mCompositionSamplingListener.unregister(mSamplingListener); + if (wrappedStopLayer != null && wrappedStopLayer.isValid()) { + wrappedStopLayer.release(); + } + }); + } + } + + private void updateMedianLuma(float medianLuma) { + mCurrentMedianLuma = medianLuma; + + // If the difference between the new luma and the current luma is larger than threshold + // then apply the current luma, this is to prevent small changes causing colors to flicker + if (Math.abs(mCurrentMedianLuma - mLastMedianLuma) + > NAVIGATION_LUMINANCE_CHANGE_THRESHOLD) { + mCallback.onRegionDarknessChanged( + medianLuma < NAVIGATION_LUMINANCE_THRESHOLD /* isRegionDark */); + mLastMedianLuma = medianLuma; + } + } + + public void updateSamplingRect() { + Rect sampledRegion = mCallback.getSampledRegion(mSampledView); + if (!mSamplingRequestBounds.equals(sampledRegion)) { + mSamplingRequestBounds.set(sampledRegion); + updateSamplingListener(); + } + } + + public void setWindowVisible(boolean visible) { + mWindowVisible = visible; + updateSamplingListener(); + } + + /** + * If we're blurring the shade window. + */ + public void setWindowHasBlurs(boolean hasBlurs) { + mWindowHasBlurs = hasBlurs; + updateSamplingListener(); + } + + public void dump(PrintWriter pw) { + dump("", pw); + } + + public void dump(String prefix, PrintWriter pw) { + pw.println(prefix + "RegionSamplingHelper:"); + pw.println(prefix + "\tsampleView isAttached: " + mSampledView.isAttachedToWindow()); + pw.println(prefix + "\tsampleView isScValid: " + (mSampledView.isAttachedToWindow() + ? mSampledView.getViewRootImpl().getSurfaceControl().isValid() + : "notAttached")); + pw.println(prefix + "\tmSamplingEnabled: " + mSamplingEnabled); + pw.println(prefix + "\tmSamplingListenerRegistered: " + mSamplingListenerRegistered); + pw.println(prefix + "\tmSamplingRequestBounds: " + mSamplingRequestBounds); + pw.println(prefix + "\tmRegisteredSamplingBounds: " + mRegisteredSamplingBounds); + pw.println(prefix + "\tmLastMedianLuma: " + mLastMedianLuma); + pw.println(prefix + "\tmCurrentMedianLuma: " + mCurrentMedianLuma); + pw.println(prefix + "\tmWindowVisible: " + mWindowVisible); + pw.println(prefix + "\tmWindowHasBlurs: " + mWindowHasBlurs); + pw.println(prefix + "\tmWaitingOnDraw: " + mWaitingOnDraw); + pw.println(prefix + "\tmRegisteredStopLayer: " + mRegisteredStopLayer); + pw.println(prefix + "\tmWrappedStopLayer: " + mWrappedStopLayer); + pw.println(prefix + "\tmIsDestroyed: " + mIsDestroyed); + } + + public interface SamplingCallback { + /** + * Called when the darkness of the sampled region changes + * @param isRegionDark true if the sampled luminance is below the luminance threshold + */ + void onRegionDarknessChanged(boolean isRegionDark); + + /** + * Get the sampled region of interest from the sampled view + * @param sampledView The view that this helper is attached to for convenience + * @return the region to be sampled in screen coordinates. Return {@code null} to avoid + * sampling in this frame + */ + Rect getSampledRegion(View sampledView); + + /** + * @return if sampling should be enabled in the current configuration + */ + default boolean isSamplingEnabled() { + return true; + } + } + + @VisibleForTesting + public static class SysuiCompositionSamplingListener { + public void register(CompositionSamplingListener listener, + int displayId, SurfaceControl stopLayer, Rect samplingArea) { + CompositionSamplingListener.register(listener, displayId, stopLayer, samplingArea); + } + + /** + * Unregisters a sampling listener. + */ + public void unregister(CompositionSamplingListener listener) { + CompositionSamplingListener.unregister(listener); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/magnetictarget/MagnetizedObject.kt index 123d4dc49199..efdc6f8cd9de 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/magnetictarget/MagnetizedObject.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 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. @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.wm.shell.common.magnetictarget + +package com.android.wm.shell.shared.magnetictarget import android.annotation.SuppressLint import android.content.Context diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java index ff2d46e11107..6c83d88032df 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip; +package com.android.wm.shell.shared.pip; import static android.util.TypedValue.COMPLEX_UNIT_DIP; @@ -29,7 +29,6 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.TypedValue; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.window.TaskSnapshot; /** @@ -75,7 +74,7 @@ public abstract class PipContentOverlay { public PipColorOverlay(Context context) { mContext = context; - mLeash = new SurfaceControl.Builder(new SurfaceSession()) + mLeash = new SurfaceControl.Builder() .setCallsite(TAG) .setName(LAYER_NAME) .setColorLayer() @@ -123,7 +122,7 @@ public abstract class PipContentOverlay { public PipSnapshotOverlay(TaskSnapshot snapshot, Rect sourceRectHint) { mSnapshot = snapshot; mSourceRectHint = new Rect(sourceRectHint); - mLeash = new SurfaceControl.Builder(new SurfaceSession()) + mLeash = new SurfaceControl.Builder() .setCallsite(TAG) .setName(LAYER_NAME) .build(); @@ -183,7 +182,7 @@ public abstract class PipContentOverlay { mBitmap = Bitmap.createBitmap(overlaySize, overlaySize, Bitmap.Config.ARGB_8888); prepareAppIconOverlay(appIcon); - mLeash = new SurfaceControl.Builder(new SurfaceSession()) + mLeash = new SurfaceControl.Builder() .setCallsite(TAG) .setName(LAYER_NAME) .build(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitBounds.java index 3e06d2d0e797..7c1faa667d9a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitBounds.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 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. @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.wm.shell.util; +package com.android.wm.shell.shared.split; import android.graphics.Rect; import android.os.Parcel; import android.os.Parcelable; -import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; +import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; import java.util.Objects; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java index e8c809e5db4a..498dc8bdd24d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.wm.shell.common.split; +package com.android.wm.shell.shared.split; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; @@ -29,6 +29,8 @@ import com.android.wm.shell.shared.TransitionUtil; public class SplitScreenConstants { /** Duration used for every split fade-in or fade-out. */ public static final int FADE_DURATION = 133; + /** Duration where we keep an app veiled to allow it to redraw itself behind the scenes. */ + public static final int VEIL_DELAY_DURATION = 300; /** Key for passing in widget intents when invoking split from launcher workspace. */ public static final String KEY_EXTRA_WIDGET_INTENT = "key_extra_widget_intent"; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/startingsurface/SplashScreenExitAnimationUtils.java index 6325c686a682..da9bf7a2cba0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/startingsurface/SplashScreenExitAnimationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2024 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.wm.shell.startingsurface; +package com.android.wm.shell.shared.startingsurface; import static android.view.Choreographer.CALLBACK_COMMIT; @@ -45,8 +45,8 @@ import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import android.window.SplashScreenView; -import com.android.wm.shell.animation.Interpolators; -import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.shared.TransactionPool; +import com.android.wm.shell.shared.animation.Interpolators; /** * Utilities for creating the splash screen window animations. @@ -88,7 +88,7 @@ public class SplashScreenExitAnimationUtils { * Creates and starts the animator to fade out the icon, reveal the app, and shift up main * window with rounded corner radius. */ - static void startAnimations(@ExitAnimationType int animationType, + public static void startAnimations(@ExitAnimationType int animationType, ViewGroup splashScreenView, SurfaceControl firstWindowSurface, int mainWindowShiftLength, TransactionPool transactionPool, Rect firstWindowFrame, int animationDuration, int iconFadeOutDuration, float iconStartAlpha, @@ -390,7 +390,8 @@ public class SplashScreenExitAnimationUtils { SurfaceControl firstWindowSurface, ViewGroup splashScreenView, TransactionPool transactionPool, Rect firstWindowFrame, int mainWindowShiftLength, float roundedCornerRadius) { - mFromYDelta = fromYDelta - Math.max(firstWindowFrame.top, roundedCornerRadius); + mFromYDelta = firstWindowFrame.top + - Math.max(firstWindowFrame.top, roundedCornerRadius); mToYDelta = toYDelta; mOccludeHoleView = occludeHoleView; mApplier = new SyncRtSurfaceTransactionApplier(occludeHoleView); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java index ef9bf008b294..514307fed4f9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java @@ -19,7 +19,7 @@ package com.android.wm.shell; import com.android.internal.protolog.LegacyProtoLogImpl; import com.android.internal.protolog.common.ILogger; import com.android.internal.protolog.common.IProtoLog; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellInit; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java index 2e5448a9e8d5..b9bf136837a6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java @@ -29,7 +29,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.sysui.ShellInit; import java.io.PrintWriter; 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 5143d419597b..9f01316d5b5c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java @@ -38,7 +38,7 @@ import android.window.SystemPerformanceHinter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.sysui.ShellInit; import java.io.PrintWriter; 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 3ded7d246499..4607a8ec1210 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -24,6 +24,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static com.android.wm.shell.compatui.impl.CompatUIEventsKt.SIZE_COMPAT_RESTART_BUTTON_APPEARED; +import static com.android.wm.shell.compatui.impl.CompatUIEventsKt.SIZE_COMPAT_RESTART_BUTTON_CLICKED; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; @@ -31,7 +33,6 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager.RunningTaskInfo; -import android.app.CameraCompatTaskInfo.CameraCompatControlState; import android.app.TaskInfo; import android.app.WindowConfiguration; import android.content.LocusId; @@ -45,18 +46,20 @@ import android.util.Log; import android.util.SparseArray; import android.view.SurfaceControl; import android.window.ITaskOrganizerController; -import android.window.ScreenCapture; import android.window.StartingWindowInfo; import android.window.StartingWindowRemovalInfo; import android.window.TaskAppearedInfo; import android.window.TaskOrganizer; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.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.compatui.CompatUIController; +import com.android.wm.shell.compatui.api.CompatUIHandler; +import com.android.wm.shell.compatui.api.CompatUIInfo; +import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonAppeared; +import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonClicked; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.startingsurface.StartingWindowController; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -69,14 +72,12 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.function.Consumer; /** * Unified task organizer for all components in the shell. * TODO(b/167582004): may consider consolidating this class and TaskOrganizer */ -public class ShellTaskOrganizer extends TaskOrganizer implements - CompatUIController.CompatUICallback { +public class ShellTaskOrganizer extends TaskOrganizer { private static final String TAG = "ShellTaskOrganizer"; // Intentionally using negative numbers here so the positive numbers can be used @@ -124,6 +125,15 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } /** + * Limited scope callback to notify when a task is removed from the system. This signal is + * not synchronized with anything (or any transition), and should not be used in cases where + * that is necessary. + */ + public interface TaskVanishedListener { + default void onTaskVanished(RunningTaskInfo taskInfo) {} + } + + /** * Callbacks for events on a task with a locus id. */ public interface LocusIdListener { @@ -167,6 +177,9 @@ public class ShellTaskOrganizer extends TaskOrganizer implements private final ArraySet<FocusListener> mFocusListeners = new ArraySet<>(); + // Listeners that should be notified when a task is removed + private final ArraySet<TaskVanishedListener> mTaskVanishedListeners = new ArraySet<>(); + private final Object mLock = new Object(); private StartingWindowController mStartingWindow; @@ -182,12 +195,11 @@ public class ShellTaskOrganizer extends TaskOrganizer implements * In charge of showing compat UI. Can be {@code null} if the device doesn't support size * compat or if this isn't the main {@link ShellTaskOrganizer}. * - * <p>NOTE: only the main {@link ShellTaskOrganizer} should have a {@link CompatUIController}, - * and register itself as a {@link CompatUIController.CompatUICallback}. Subclasses should be - * initialized with a {@code null} {@link CompatUIController}. + * <p>NOTE: only the main {@link ShellTaskOrganizer} should have a {@link CompatUIHandler}, + * Subclasses should be initialized with a {@code null} {@link CompatUIHandler}. */ @Nullable - private final CompatUIController mCompatUI; + private final CompatUIHandler mCompatUI; @NonNull private final ShellCommandHandler mShellCommandHandler; @@ -211,7 +223,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements public ShellTaskOrganizer(ShellInit shellInit, ShellCommandHandler shellCommandHandler, - @Nullable CompatUIController compatUI, + @Nullable CompatUIHandler compatUI, Optional<UnfoldAnimationController> unfoldAnimationController, Optional<RecentTasksController> recentTasks, ShellExecutor mainExecutor) { @@ -223,7 +235,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements protected ShellTaskOrganizer(ShellInit shellInit, ShellCommandHandler shellCommandHandler, ITaskOrganizerController taskOrganizerController, - @Nullable CompatUIController compatUI, + @Nullable CompatUIHandler compatUI, Optional<UnfoldAnimationController> unfoldAnimationController, Optional<RecentTasksController> recentTasks, ShellExecutor mainExecutor) { @@ -240,7 +252,18 @@ public class ShellTaskOrganizer extends TaskOrganizer implements private void onInit() { mShellCommandHandler.addDumpCallback(this::dump, this); if (mCompatUI != null) { - mCompatUI.setCompatUICallback(this); + mCompatUI.setCallback(compatUIEvent -> { + switch(compatUIEvent.getEventId()) { + case SIZE_COMPAT_RESTART_BUTTON_APPEARED: + onSizeCompatRestartButtonAppeared(compatUIEvent.asType()); + break; + case SIZE_COMPAT_RESTART_BUTTON_CLICKED: + onSizeCompatRestartButtonClicked(compatUIEvent.asType()); + break; + default: + + } + }); } registerOrganizer(); } @@ -409,7 +432,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } /** - * Removes listener. + * Removes a locus id listener. */ public void removeLocusIdListener(LocusIdListener listener) { synchronized (mLock) { @@ -430,7 +453,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } /** - * Removes listener. + * Removes a focus listener. */ public void removeFocusListener(FocusListener listener) { synchronized (mLock) { @@ -439,6 +462,24 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } /** + * Adds a listener to be notified when a task vanishes. + */ + public void addTaskVanishedListener(TaskVanishedListener listener) { + synchronized (mLock) { + mTaskVanishedListeners.add(listener); + } + } + + /** + * Removes a task-vanished listener. + */ + public void removeTaskVanishedListener(TaskVanishedListener listener) { + synchronized (mLock) { + mTaskVanishedListeners.remove(listener); + } + } + + /** * Returns a surface which can be used to attach overlays to the home root task */ @NonNull @@ -517,19 +558,6 @@ public class ShellTaskOrganizer extends TaskOrganizer implements mRecentTasks.ifPresent(recentTasks -> recentTasks.onTaskAdded(info.getTaskInfo())); } - /** - * Take a screenshot of a task. - */ - public void screenshotTask(RunningTaskInfo taskInfo, Rect crop, - Consumer<ScreenCapture.ScreenshotHardwareBuffer> consumer) { - final TaskAppearedInfo info = mTasks.get(taskInfo.taskId); - if (info == null) { - return; - } - ScreenshotUtils.captureLayer(info.getLeash(), crop, consumer); - } - - @Override public void onTaskInfoChanged(RunningTaskInfo taskInfo) { synchronized (mLock) { @@ -556,7 +584,8 @@ public class ShellTaskOrganizer extends TaskOrganizer implements final boolean windowModeChanged = data.getTaskInfo().getWindowingMode() != taskInfo.getWindowingMode(); final boolean visibilityChanged = data.getTaskInfo().isVisible != taskInfo.isVisible; - if (windowModeChanged || visibilityChanged) { + if (windowModeChanged || (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM + && visibilityChanged)) { mRecentTasks.ifPresent(recentTasks -> recentTasks.onTaskRunningInfoChanged(taskInfo)); } @@ -614,6 +643,9 @@ public class ShellTaskOrganizer extends TaskOrganizer implements t.apply(); ProtoLog.v(WM_SHELL_TASK_ORG, "Removing overlay surface"); } + for (TaskVanishedListener l : mTaskVanishedListeners) { + l.onTaskVanished(taskInfo); + } if (!ENABLE_SHELL_TRANSITIONS && (appearedInfo.getLeash() != null)) { // Preemptively clean up the leash only if shell transitions are not enabled @@ -638,6 +670,15 @@ public class ShellTaskOrganizer extends TaskOrganizer implements return result; } + /** Return list of {@link RunningTaskInfo}s on all the displays. */ + public ArrayList<RunningTaskInfo> getRunningTasks() { + ArrayList<RunningTaskInfo> result = new ArrayList<>(); + for (int i = 0; i < mTasks.size(); i++) { + result.add(mTasks.valueAt(i).getTaskInfo()); + } + return result; + } + /** Gets running task by taskId. Returns {@code null} if no such task observed. */ @Nullable public RunningTaskInfo getRunningTaskInfo(int taskId) { @@ -647,6 +688,22 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } } + /** + * Shows/hides the given task surface. Not for general use as changing the task visibility may + * conflict with other Transitions. This is currently ONLY used to temporarily hide a task + * while a drag is in session. + */ + public void setTaskSurfaceVisibility(int taskId, boolean visible) { + synchronized (mLock) { + final TaskAppearedInfo info = mTasks.get(taskId); + if (info != null) { + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.setVisibility(info.getLeash(), visible); + t.apply(); + } + } + } + private boolean updateTaskListenerIfNeeded(RunningTaskInfo taskInfo, SurfaceControl leash, TaskListener oldListener, TaskListener newListener) { if (oldListener == newListener) return false; @@ -694,21 +751,26 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } } - @Override - public void onSizeCompatRestartButtonAppeared(int taskId) { - final TaskAppearedInfo info; + /** Reparents a child window surface to the task surface. */ + public void reparentChildSurfaceToTask(int taskId, SurfaceControl sc, + SurfaceControl.Transaction t) { + final TaskListener taskListener; synchronized (mLock) { - info = mTasks.get(taskId); + taskListener = mTasks.contains(taskId) + ? getTaskListener(mTasks.get(taskId).getTaskInfo()) + : null; } - if (info == null) { + if (taskListener == null) { + ProtoLog.w(WM_SHELL_TASK_ORG, "Failed to find Task to reparent surface taskId=%d", + taskId); return; } - logSizeCompatRestartButtonEventReported(info, - FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED__EVENT__APPEARED); + taskListener.reparentChildSurfaceToTask(taskId, sc, t); } - @Override - public void onSizeCompatRestartButtonClicked(int taskId) { + @VisibleForTesting + void onSizeCompatRestartButtonAppeared(@NonNull SizeCompatRestartButtonAppeared compatUIEvent) { + final int taskId = compatUIEvent.getTaskId(); final TaskAppearedInfo info; synchronized (mLock) { info = mTasks.get(taskId); @@ -717,12 +779,12 @@ public class ShellTaskOrganizer extends TaskOrganizer implements return; } logSizeCompatRestartButtonEventReported(info, - FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED__EVENT__CLICKED); - restartTaskTopActivityProcessIfVisible(info.getTaskInfo().token); + FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED__EVENT__APPEARED); } - @Override - public void onCameraControlStateUpdated(int taskId, @CameraCompatControlState int state) { + @VisibleForTesting + void onSizeCompatRestartButtonClicked(@NonNull SizeCompatRestartButtonClicked compatUIEvent) { + final int taskId = compatUIEvent.getTaskId(); final TaskAppearedInfo info; synchronized (mLock) { info = mTasks.get(taskId); @@ -730,24 +792,9 @@ public class ShellTaskOrganizer extends TaskOrganizer implements if (info == null) { return; } - updateCameraCompatControlState(info.getTaskInfo().token, state); - } - - /** Reparents a child window surface to the task surface. */ - public void reparentChildSurfaceToTask(int taskId, SurfaceControl sc, - SurfaceControl.Transaction t) { - final TaskListener taskListener; - synchronized (mLock) { - taskListener = mTasks.contains(taskId) - ? getTaskListener(mTasks.get(taskId).getTaskInfo()) - : null; - } - if (taskListener == null) { - ProtoLog.w(WM_SHELL_TASK_ORG, "Failed to find Task to reparent surface taskId=%d", - taskId); - return; - } - taskListener.reparentChildSurfaceToTask(taskId, sc, t); + logSizeCompatRestartButtonEventReported(info, + FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED__EVENT__CLICKED); + restartTaskTopActivityProcessIfVisible(info.getTaskInfo().token); } private void logSizeCompatRestartButtonEventReported(@NonNull TaskAppearedInfo info, @@ -777,10 +824,10 @@ public class ShellTaskOrganizer extends TaskOrganizer implements // on this Task if there is any. if (taskListener == null || !taskListener.supportCompatUI() || !taskInfo.appCompatTaskInfo.hasCompatUI() || !taskInfo.isVisible) { - mCompatUI.onCompatInfoChanged(taskInfo, null /* taskListener */); + mCompatUI.onCompatInfoChanged(new CompatUIInfo(taskInfo, null /* taskListener */)); return; } - mCompatUI.onCompatInfoChanged(taskInfo, taskListener); + mCompatUI.onCompatInfoChanged(new CompatUIInfo(taskInfo, taskListener)); } private TaskListener getTaskListener(RunningTaskInfo runningTaskInfo) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java index 8d30db64a3e5..53551387230c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java @@ -18,6 +18,7 @@ package com.android.wm.shell.activityembedding; import static android.graphics.Matrix.MTRANS_X; import static android.graphics.Matrix.MTRANS_Y; +import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import android.annotation.CallSuper; import android.graphics.Point; @@ -146,6 +147,14 @@ class ActivityEmbeddingAnimationAdapter { /** To be overridden by subclasses to adjust the animation surface change. */ void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { // Update the surface position and alpha. + if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader() + && mAnimation.getExtensionEdges() != 0x0 + && !(mChange.hasFlags(FLAG_TRANSLUCENT) + && mChange.getActivityComponent() != null)) { + // Extend non-translucent activities + t.setEdgeExtensionEffect(mLeash, mAnimation.getExtensionEdges()); + } + mTransformation.getMatrix().postTranslate(mContentRelOffset.x, mContentRelOffset.y); t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); t.setAlpha(mLeash, mTransformation.getAlpha()); @@ -165,7 +174,7 @@ class ActivityEmbeddingAnimationAdapter { if (!cropRect.intersect(mWholeAnimationBounds)) { // Hide the surface when it is outside of the animation area. t.setAlpha(mLeash, 0); - } else if (mAnimation.hasExtension()) { + } else if (mAnimation.getExtensionEdges() != 0) { // Allow the surface to be shown in its original bounds in case we want to use edge // extensions. cropRect.union(mContentBounds); @@ -180,6 +189,10 @@ class ActivityEmbeddingAnimationAdapter { @CallSuper void onAnimationEnd(@NonNull SurfaceControl.Transaction t) { onAnimationUpdate(t, mAnimation.getDuration()); + if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader() + && mAnimation.getExtensionEdges() != 0x0) { + t.setEdgeExtensionEffect(mLeash, /* edge */ 0); + } } final long getDurationHint() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java index a426b206b0cd..d2cef4baf798 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java @@ -31,6 +31,7 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_ import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; +import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; import android.util.ArraySet; @@ -45,6 +46,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; import com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationAdapter.SnapshotAdapter; import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.shared.TransitionUtil; @@ -142,8 +144,10 @@ class ActivityEmbeddingAnimationRunner { // ending states. prepareForJumpCut(info, startTransaction); } else { - addEdgeExtensionIfNeeded(startTransaction, finishTransaction, - postStartTransactionCallbacks, adapters); + if (!com.android.graphics.libgui.flags.Flags.edgeExtensionShader()) { + addEdgeExtensionIfNeeded(startTransaction, finishTransaction, + postStartTransactionCallbacks, adapters); + } addBackgroundColorIfNeeded(info, startTransaction, finishTransaction, adapters); for (ActivityEmbeddingAnimationAdapter adapter : adapters) { duration = Math.max(duration, adapter.getDurationHint()); @@ -263,7 +267,10 @@ class ActivityEmbeddingAnimationRunner { for (TransitionInfo.Change change : openingChanges) { final Animation animation = animationProvider.get(info, change, openingWholeScreenBounds); - if (animation.getDuration() == 0) { + if (shouldUseJumpCutForAnimation(animation)) { + if (Flags.activityEmbeddingAnimationCustomizationFlag()) { + return new ArrayList<>(); + } continue; } final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter( @@ -288,7 +295,10 @@ class ActivityEmbeddingAnimationRunner { } final Animation animation = animationProvider.get(info, change, closingWholeScreenBounds); - if (animation.getDuration() == 0) { + if (shouldUseJumpCutForAnimation(animation)) { + if (Flags.activityEmbeddingAnimationCustomizationFlag()) { + return new ArrayList<>(); + } continue; } final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter( @@ -333,7 +343,7 @@ class ActivityEmbeddingAnimationRunner { @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) { for (ActivityEmbeddingAnimationAdapter adapter : adapters) { final Animation animation = adapter.mAnimation; - if (!animation.hasExtension()) { + if (animation.getExtensionEdges() == 0) { continue; } if (adapter.mChange.hasFlags(FLAG_TRANSLUCENT) @@ -398,7 +408,15 @@ class ActivityEmbeddingAnimationRunner { // This is because the TaskFragment surface/change won't contain the Activity's before its // reparent. Animation changeAnimation = null; - Rect parentBounds = new Rect(); + final Rect parentBounds = new Rect(); + // We use a single boolean value to record the backdrop override because the override used + // for overlay and we restrict to single overlay animation. We should fix the assumption + // if we allow multiple overlay transitions. + // The backdrop logic is mainly for animations of split animations. The backdrop should be + // disabled if there is any open/close target in the same transition as the change target. + // However, the overlay change animation usually contains one change target, and shows + // backdrop unexpectedly. + Boolean overrideShowBackdrop = null; for (TransitionInfo.Change change : info.getChanges()) { if (change.getMode() != TRANSIT_CHANGE || change.getStartAbsBounds().equals(change.getEndAbsBounds())) { @@ -421,21 +439,29 @@ class ActivityEmbeddingAnimationRunner { } } - // The TaskFragment may be enter/exit split, so we take the union of both as the parent - // size. - parentBounds.union(boundsAnimationChange.getStartAbsBounds()); - parentBounds.union(boundsAnimationChange.getEndAbsBounds()); - if (boundsAnimationChange != change) { - // Union the change starting bounds in case the activity is resized and reparented - // to a TaskFragment. In that case, the TaskFragment may not cover the activity's - // starting bounds. - parentBounds.union(change.getStartAbsBounds()); + final TransitionInfo.AnimationOptions options = boundsAnimationChange + .getAnimationOptions(); + if (options != null) { + final Animation overrideAnimation = mAnimationSpec.loadCustomAnimationFromOptions( + options, TRANSIT_CHANGE); + if (overrideAnimation != null) { + overrideShowBackdrop = overrideAnimation.getShowBackdrop(); + } } + calculateParentBounds(change, boundsAnimationChange, parentBounds); // There are two animations in the array. The first one is for the start leash // (snapshot), and the second one is for the end leash (TaskFragment). - final Animation[] animations = mAnimationSpec.createChangeBoundsChangeAnimations(change, - parentBounds); + final Animation[] animations = + mAnimationSpec.createChangeBoundsChangeAnimations(info, change, parentBounds); + // Jump cut if either animation has zero for duration. + if (Flags.activityEmbeddingAnimationCustomizationFlag()) { + for (Animation animation : animations) { + if (shouldUseJumpCutForAnimation(animation)) { + return new ArrayList<>(); + } + } + } // Keep track as we might need to add background color for the animation. // Although there may be multiple change animation, record one of them is sufficient // because the background color will be added to the root leash for the whole animation. @@ -466,7 +492,7 @@ class ActivityEmbeddingAnimationRunner { // If there is no corresponding open/close window with the change, we should show background // color to cover the empty part of the screen. - boolean shouldShouldBackgroundColor = true; + boolean shouldShowBackgroundColor = true; // Handle the other windows that don't have bounds change in the same transition. for (TransitionInfo.Change change : info.getChanges()) { if (handledChanges.contains(change)) { @@ -482,17 +508,26 @@ class ActivityEmbeddingAnimationRunner { // window without bounds change. animation = ActivityEmbeddingAnimationSpec.createNoopAnimation(change); } else if (TransitionUtil.isClosingType(change.getMode())) { - animation = mAnimationSpec.createChangeBoundsCloseAnimation(change, parentBounds); - shouldShouldBackgroundColor = false; + animation = + mAnimationSpec.createChangeBoundsCloseAnimation(info, change, parentBounds); + shouldShowBackgroundColor = false; } else { - animation = mAnimationSpec.createChangeBoundsOpenAnimation(change, parentBounds); - shouldShouldBackgroundColor = false; + animation = + mAnimationSpec.createChangeBoundsOpenAnimation(info, change, parentBounds); + shouldShowBackgroundColor = false; + } + if (Flags.activityEmbeddingAnimationCustomizationFlag()) { + if (shouldUseJumpCutForAnimation(animation)) { + return new ArrayList<>(); + } } adapters.add(new ActivityEmbeddingAnimationAdapter(animation, change, TransitionUtil.getRootFor(change, info))); } - if (shouldShouldBackgroundColor && changeAnimation != null) { + shouldShowBackgroundColor = overrideShowBackdrop != null + ? overrideShowBackdrop : shouldShowBackgroundColor; + if (shouldShowBackgroundColor && changeAnimation != null) { // Change animation may leave part of the screen empty. Show background color to cover // that. changeAnimation.setShowBackdrop(true); @@ -502,6 +537,39 @@ class ActivityEmbeddingAnimationRunner { } /** + * Calculates parent bounds of the animation target by {@code change}. + */ + @VisibleForTesting + static void calculateParentBounds(@NonNull TransitionInfo.Change change, + @NonNull TransitionInfo.Change boundsAnimationChange, @NonNull Rect outParentBounds) { + if (Flags.activityEmbeddingOverlayPresentationFlag()) { + final Point endParentSize = change.getEndParentSize(); + if (endParentSize.equals(0, 0)) { + return; + } + final Point endRelPosition = change.getEndRelOffset(); + final Point endAbsPosition = new Point(change.getEndAbsBounds().left, + change.getEndAbsBounds().top); + final Point parentEndAbsPosition = new Point(endAbsPosition.x - endRelPosition.x, + endAbsPosition.y - endRelPosition.y); + outParentBounds.set(parentEndAbsPosition.x, parentEndAbsPosition.y, + parentEndAbsPosition.x + endParentSize.x, + parentEndAbsPosition.y + endParentSize.y); + } else { + // The TaskFragment may be enter/exit split, so we take the union of both as + // the parent size. + outParentBounds.union(boundsAnimationChange.getStartAbsBounds()); + outParentBounds.union(boundsAnimationChange.getEndAbsBounds()); + if (boundsAnimationChange != change) { + // Union the change starting bounds in case the activity is resized and + // reparented to a TaskFragment. In that case, the TaskFragment may not cover + // the activity's starting bounds. + outParentBounds.union(change.getStartAbsBounds()); + } + } + } + + /** * Takes a screenshot of the given {@code screenshotChange} surface if WM Core hasn't taken one. * The screenshot leash should be attached to the {@code animationChange} surface which we will * animate later. @@ -595,6 +663,12 @@ class ActivityEmbeddingAnimationRunner { return true; } + /** Whether or not to use jump cut based on the animation. */ + @VisibleForTesting + static boolean shouldUseJumpCutForAnimation(@NonNull Animation animation) { + return animation.getDuration() == 0; + } + /** Updates the changes to end states in {@code startTransaction} for jump cut animation. */ private void prepareForJumpCut(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java index b9868629e64b..3046307702c2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java @@ -18,6 +18,8 @@ package com.android.wm.shell.activityembedding; import static android.app.ActivityOptions.ANIM_CUSTOM; +import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.window.TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE; import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation; @@ -27,6 +29,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.graphics.Rect; +import android.util.Log; +import android.view.WindowManager; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.AnimationSet; @@ -42,7 +46,6 @@ import com.android.window.flags.Flags; import com.android.wm.shell.shared.TransitionUtil; /** Animation spec for ActivityEmbedding transition. */ -// TODO(b/206557124): provide an easier way to customize animation class ActivityEmbeddingAnimationSpec { private static final String TAG = "ActivityEmbeddingAnimSpec"; @@ -91,8 +94,14 @@ class ActivityEmbeddingAnimationSpec { /** Animation for window that is opening in a change transition. */ @NonNull - Animation createChangeBoundsOpenAnimation(@NonNull TransitionInfo.Change change, - @NonNull Rect parentBounds) { + Animation createChangeBoundsOpenAnimation(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, @NonNull Rect parentBounds) { + if (Flags.activityEmbeddingAnimationCustomizationFlag()) { + final Animation customAnimation = loadCustomAnimation(info, change, TRANSIT_CHANGE); + if (customAnimation != null) { + return customAnimation; + } + } // Use end bounds for opening. final Rect bounds = change.getEndAbsBounds(); final int startLeft; @@ -119,8 +128,14 @@ class ActivityEmbeddingAnimationSpec { /** Animation for window that is closing in a change transition. */ @NonNull - Animation createChangeBoundsCloseAnimation(@NonNull TransitionInfo.Change change, - @NonNull Rect parentBounds) { + Animation createChangeBoundsCloseAnimation(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, @NonNull Rect parentBounds) { + if (Flags.activityEmbeddingAnimationCustomizationFlag()) { + final Animation customAnimation = loadCustomAnimation(info, change, TRANSIT_CHANGE); + if (customAnimation != null) { + return customAnimation; + } + } // Use start bounds for closing. final Rect bounds = change.getStartAbsBounds(); final int endTop; @@ -151,8 +166,17 @@ class ActivityEmbeddingAnimationSpec { * the second one is for the end leash. */ @NonNull - Animation[] createChangeBoundsChangeAnimations(@NonNull TransitionInfo.Change change, - @NonNull Rect parentBounds) { + Animation[] createChangeBoundsChangeAnimations(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change change, @NonNull Rect parentBounds) { + if (Flags.activityEmbeddingAnimationCustomizationFlag()) { + // TODO(b/293658614): Support more complicated animations that may need more than a noop + // animation as the start leash. + final Animation noopAnimation = createNoopAnimation(change); + final Animation customAnimation = loadCustomAnimation(info, change, TRANSIT_CHANGE); + if (customAnimation != null) { + return new Animation[]{noopAnimation, customAnimation}; + } + } // Both start bounds and end bounds are in screen coordinates. We will post translate // to the local coordinates in ActivityEmbeddingAnimationAdapter#onAnimationUpdate final Rect startBounds = change.getStartAbsBounds(); @@ -203,7 +227,7 @@ class ActivityEmbeddingAnimationSpec { Animation loadOpenAnimation(@NonNull TransitionInfo info, @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) { final boolean isEnter = TransitionUtil.isOpeningType(change.getMode()); - final Animation customAnimation = loadCustomAnimation(info, change, isEnter); + final Animation customAnimation = loadCustomAnimation(info, change, change.getMode()); final Animation animation; if (customAnimation != null) { animation = customAnimation; @@ -230,7 +254,7 @@ class ActivityEmbeddingAnimationSpec { Animation loadCloseAnimation(@NonNull TransitionInfo info, @NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) { final boolean isEnter = TransitionUtil.isOpeningType(change.getMode()); - final Animation customAnimation = loadCustomAnimation(info, change, isEnter); + final Animation customAnimation = loadCustomAnimation(info, change, change.getMode()); final Animation animation; if (customAnimation != null) { animation = customAnimation; @@ -263,18 +287,46 @@ class ActivityEmbeddingAnimationSpec { @Nullable private Animation loadCustomAnimation(@NonNull TransitionInfo info, - @NonNull TransitionInfo.Change change, boolean isEnter) { + @NonNull TransitionInfo.Change change, @WindowManager.TransitionType int mode) { final TransitionInfo.AnimationOptions options; if (Flags.moveAnimationOptionsToChange()) { options = change.getAnimationOptions(); } else { options = info.getAnimationOptions(); } + return loadCustomAnimationFromOptions(options, mode); + } + + @Nullable + Animation loadCustomAnimationFromOptions(@Nullable TransitionInfo.AnimationOptions options, + @WindowManager.TransitionType int mode) { if (options == null || options.getType() != ANIM_CUSTOM) { return null; } - final Animation anim = mTransitionAnimation.loadAnimationRes(options.getPackageName(), - isEnter ? options.getEnterResId() : options.getExitResId()); + final int resId; + if (TransitionUtil.isOpeningType(mode)) { + resId = options.getEnterResId(); + } else if (TransitionUtil.isClosingType(mode)) { + resId = options.getExitResId(); + } else if (mode == TRANSIT_CHANGE) { + resId = options.getChangeResId(); + } else { + Log.w(TAG, "Unknown transit type:" + mode); + resId = DEFAULT_ANIMATION_RESOURCES_ID; + } + // Use the default animation if the resources ID is not specified. + if (resId == DEFAULT_ANIMATION_RESOURCES_ID) { + return null; + } + + final Animation anim; + if (Flags.activityEmbeddingAnimationCustomizationFlag()) { + // TODO(b/293658614): Consider allowing custom animations from non-default packages. + // Enforce limiting to animations from the default "android" package for now. + anim = mTransitionAnimation.loadDefaultAnimationRes(resId); + } else { + anim = mTransitionAnimation.loadAnimationRes(options.getPackageName(), resId); + } if (anim != null) { return anim; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/FlingAnimationUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/FlingAnimationUtils.java index 26edd7d2268b..be1f71e939be 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/FlingAnimationUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/FlingAnimationUtils.java @@ -23,6 +23,8 @@ import android.view.ViewPropertyAnimator; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; +import com.android.wm.shell.shared.animation.Interpolators; + import javax.inject.Inject; /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebGenericLinksParser.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebGenericLinksParser.kt new file mode 100644 index 000000000000..56447de6bae1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebGenericLinksParser.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2024 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.apptoweb + +import android.content.Context +import android.provider.DeviceConfig +import android.webkit.URLUtil +import com.android.internal.annotations.VisibleForTesting +import com.android.wm.shell.R +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.useAppToWebBuildTimeGenericLinks + +/** + * Retrieves the build-time or server-side generic links list and parses and stores the + * package-to-url pairs. + */ +class AppToWebGenericLinksParser( + private val context: Context, + @ShellMainThread private val mainExecutor: ShellExecutor +) { + private val genericLinksMap: MutableMap<String, String> = mutableMapOf() + + init { + // If using the server-side generic links list, register a listener + if (!useAppToWebBuildTimeGenericLinks()) { + DeviceConfigListener() + } + + updateGenericLinksMap() + } + + /** Returns the generic link associated with the [packageName] or null if there is none. */ + fun getGenericLink(packageName: String): String? = genericLinksMap[packageName] + + private fun updateGenericLinksMap() { + val genericLinksList = + if (useAppToWebBuildTimeGenericLinks()) { + context.resources.getString(R.string.generic_links_list) + } else { + DeviceConfig.getString(NAMESPACE, FLAG_GENERIC_LINKS, /* defaultValue= */ "") + } ?: return + + parseGenericLinkList(genericLinksList) + } + + private fun parseGenericLinkList(genericLinksList: String) { + val newEntries = + genericLinksList + .split(" ") + .filter { it.contains(':') } + .map { + val (packageName, url) = it.split(':', limit = 2) + return@map packageName to url + } + .filter { URLUtil.isNetworkUrl(it.second) } + + genericLinksMap.clear() + genericLinksMap.putAll(newEntries) + } + + /** + * Listens for changes to the server-side generic links list and updates the package to url map + * if [DesktopModeStatus#useBuildTimeGenericLinkList()] is set to false. + */ + inner class DeviceConfigListener : DeviceConfig.OnPropertiesChangedListener { + init { + DeviceConfig.addOnPropertiesChangedListener(NAMESPACE, mainExecutor, this) + } + + override fun onPropertiesChanged(properties: DeviceConfig.Properties) { + if (properties.keyset.contains(FLAG_GENERIC_LINKS)) { + updateGenericLinksMap() + } + } + } + + companion object { + private const val NAMESPACE = DeviceConfig.NAMESPACE_APP_COMPAT_OVERRIDES + @VisibleForTesting const val FLAG_GENERIC_LINKS = "generic_links_flag" + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt new file mode 100644 index 000000000000..05ce36120c4f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 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. + */ + +@file:JvmName("AppToWebUtils") + +package com.android.wm.shell.apptoweb + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri + +private val browserIntent = Intent() + .setAction(Intent.ACTION_VIEW) + .addCategory(Intent.CATEGORY_BROWSABLE) + .setData(Uri.parse("http:")) + +/** + * Returns a boolean indicating whether a given package is a browser app. + */ +fun isBrowserApp(context: Context, packageName: String, userId: Int): Boolean { + browserIntent.setPackage(packageName) + val list = context.packageManager.queryIntentActivitiesAsUser( + browserIntent, PackageManager.MATCH_ALL, userId + ) + + list.forEach { + if (it.activityInfo != null && it.handleAllWebDataURI) { + return true + } + } + return false +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AssistContentRequester.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AssistContentRequester.kt new file mode 100644 index 000000000000..249185eca323 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AssistContentRequester.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2024 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.apptoweb + +import android.app.ActivityTaskManager +import android.app.IActivityTaskManager +import android.app.IAssistDataReceiver +import android.app.assist.AssistContent +import android.content.Context +import android.graphics.Bitmap +import android.os.Bundle +import android.os.RemoteException +import android.util.Slog +import java.lang.ref.WeakReference +import java.util.Collections +import java.util.WeakHashMap +import java.util.concurrent.Executor + +/** + * Can be used to request the AssistContent from a provided task id, useful for getting the web uri + * if provided from the task. + */ +class AssistContentRequester( + context: Context, + private val callBackExecutor: Executor, + private val systemInteractionExecutor: Executor +) { + interface Callback { + // Called when the [AssistContent] of the requested task is available. + fun onAssistContentAvailable(assistContent: AssistContent?) + } + + private val activityTaskManager: IActivityTaskManager = ActivityTaskManager.getService() + private val attributionTag: String? = context.attributionTag + private val packageName: String = context.applicationContext.packageName + + // If system loses the callback, our internal cache of original callback will also get cleared. + private val pendingCallbacks = Collections.synchronizedMap(WeakHashMap<Any, Callback>()) + + /** + * Request the [AssistContent] from the task with the provided id. + * + * @param taskId to query for the content. + * @param callback to call when the content is available, called on the main thread. + */ + fun requestAssistContent(taskId: Int, callback: Callback) { + // ActivityTaskManager interaction here is synchronous, so call off the main thread. + systemInteractionExecutor.execute { + try { + val success = activityTaskManager.requestAssistDataForTask( + AssistDataReceiver(callback, this), + taskId, + packageName, + attributionTag, + false /* fetchStructure */ + ) + if (!success) { + executeOnMainExecutor { callback.onAssistContentAvailable(null) } + } + } catch (e: RemoteException) { + Slog.e(TAG, "Requesting assist content failed for task: $taskId", e) + } + } + } + + private fun executeOnMainExecutor(callback: Runnable) { + callBackExecutor.execute(callback) + } + + private class AssistDataReceiver( + callback: Callback, + parent: AssistContentRequester + ) : IAssistDataReceiver.Stub() { + // The AssistDataReceiver binder callback object is passed to a system server, that may + // keep hold of it for longer than the lifetime of the AssistContentRequester object, + // potentially causing a memory leak. In the callback passed to the system server, only + // keep a weak reference to the parent object and lookup its callback if it still exists. + private val parentRef: WeakReference<AssistContentRequester> + private val callbackKey = Any() + + init { + parent.pendingCallbacks[callbackKey] = callback + parentRef = WeakReference(parent) + } + + override fun onHandleAssistData(data: Bundle?) { + val content = data?.getParcelable(ASSIST_KEY_CONTENT, AssistContent::class.java) + if (content == null) { + Slog.d(TAG, "Received AssistData, but no AssistContent found") + return + } + val requester = parentRef.get() + if (requester != null) { + val callback = requester.pendingCallbacks[callbackKey] + if (callback != null) { + requester.executeOnMainExecutor { callback.onAssistContentAvailable(content) } + } else { + Slog.d(TAG, "Callback received after calling UI was disposed of") + } + } else { + Slog.d(TAG, "Callback received after Requester was collected") + } + } + + override fun onHandleAssistScreenshot(screenshot: Bitmap) {} + } + + companion object { + private const val TAG = "AssistContentRequester" + private const val ASSIST_KEY_CONTENT = "content" + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OWNERS new file mode 100644 index 000000000000..6207e5b020f7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OWNERS @@ -0,0 +1,8 @@ +atsjenk@google.com +jorgegil@google.com +madym@google.com +mattsziklay@google.com +mdehaini@google.com +pbdr@google.com +tkachenkoi@google.com +vaniadesmonda@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java index 196f89d5794e..df80946a99aa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java @@ -107,4 +107,24 @@ public interface BackAnimation { * @param pilferCallback the callback to pilfer pointers. */ void setPilferPointerCallback(Runnable pilferCallback); + + /** + * Set a callback to requestTopUi. + * @param topUiRequest the callback to requestTopUi. + */ + void setTopUiRequestCallback(TopUiRequest topUiRequest); + + /** + * Callback to request SysUi to call + * {@link android.app.IActivityManager#setHasTopUi(boolean)}. + */ + interface TopUiRequest { + + /** + * Request {@link android.app.IActivityManager#setHasTopUi(boolean)} to be called. + * @param requestTopUi whether topUi should be requested or not + * @param tag tag of the request-source + */ + void requestTopUi(boolean requestTopUi, String tag); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java index d754d04e6b33..26f7b360e0fc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java @@ -20,6 +20,7 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; import android.annotation.NonNull; +import android.annotation.Nullable; import android.graphics.Color; import android.graphics.Rect; import android.view.SurfaceControl; @@ -59,6 +60,23 @@ public class BackAnimationBackground { */ public void ensureBackground(Rect startRect, int color, @NonNull SurfaceControl.Transaction transaction, int statusbarHeight) { + ensureBackground(startRect, color, transaction, statusbarHeight, + null /* cropBounds */, 0 /* cornerRadius */); + } + + /** + * Ensures the back animation background color layer is present. + * + * @param startRect The start bounds of the closing target. + * @param color The background color. + * @param transaction The animation transaction. + * @param statusbarHeight The height of the statusbar (in px). + * @param cropBounds The crop bounds of the surface, set to non-empty to show wallpaper. + * @param cornerRadius The radius of corner, only work when cropBounds is not empty. + */ + public void ensureBackground(Rect startRect, int color, + @NonNull SurfaceControl.Transaction transaction, int statusbarHeight, + @Nullable Rect cropBounds, float cornerRadius) { if (mBackgroundSurface != null) { return; } @@ -78,6 +96,10 @@ public class BackAnimationBackground { transaction.setColor(mBackgroundSurface, colorComponents) .setLayer(mBackgroundSurface, BACKGROUND_LAYER) .show(mBackgroundSurface); + if (cropBounds != null && !cropBounds.isEmpty()) { + transaction.setCrop(mBackgroundSurface, cropBounds) + .setCornerRadius(mBackgroundSurface, cornerRadius); + } mStartBounds = startRect; mIsRequestingStatusBarAppearance = false; mStatusbarHeight = statusbarHeight; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 7041ea307b0f..5836085e0ddc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -16,25 +16,39 @@ package com.android.wm.shell.back; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.view.RemoteAnimationTarget.MODE_CLOSING; +import static android.view.RemoteAnimationTarget.MODE_OPENING; +import static android.view.WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION; +import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; +import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; +import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; +import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; + import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME; +import static com.android.window.flags.Flags.migratePredictiveBackTransition; import static com.android.window.flags.Flags.predictiveBackSystemAnims; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; -import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION; +import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityTaskManager; import android.app.IActivityTaskManager; +import android.app.TaskInfo; +import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.res.Configuration; import android.database.ContentObserver; +import android.graphics.Point; import android.graphics.Rect; import android.hardware.input.InputManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.os.IBinder; import android.os.RemoteCallback; import android.os.RemoteException; import android.os.SystemClock; @@ -48,6 +62,7 @@ import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; import android.view.WindowManager; import android.window.BackAnimationAdapter; import android.window.BackEvent; @@ -57,24 +72,31 @@ import android.window.BackTouchTracker; import android.window.IBackAnimationFinishedCallback; import android.window.IBackAnimationRunner; import android.window.IOnBackInvokedCallback; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.internal.util.LatencyTracker; import com.android.internal.view.AppearanceRegion; import com.android.wm.shell.R; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; /** * Controls the window animation run when a user initiates a back gesture. @@ -95,12 +117,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont */ private static final long MAX_ANIMATION_DURATION = 2000; private final LatencyTracker mLatencyTracker; + @ShellMainThread private final Handler mHandler; /** True when a back gesture is ongoing */ private boolean mBackGestureStarted = false; /** Tracks if an uninterruptible animation is in progress */ private boolean mPostCommitAnimationInProgress = false; + private boolean mRealCallbackInvoked = false; /** Tracks if we should start the back gesture on the next motion move event */ private boolean mShouldStartOnNextMoveEvent = false; @@ -114,6 +138,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @Nullable private BackNavigationInfo mBackNavigationInfo; + private boolean mReceivedNullNavigationInfo = false; private final IActivityTaskManager mActivityTaskManager; private final Context mContext; private final ContentResolver mContentResolver; @@ -122,6 +147,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private final ShellExecutor mShellExecutor; private final Handler mBgHandler; private final WindowManager mWindowManager; + private final Transitions mTransitions; + @VisibleForTesting + final BackTransitionHandler mBackTransitionHandler; @VisibleForTesting final Rect mTouchableArea = new Rect(); @@ -149,7 +177,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @Nullable private IOnBackInvokedCallback mActiveCallback; @Nullable - private RemoteAnimationTarget[] mApps; + @VisibleForTesting + RemoteAnimationTarget[] mApps; @VisibleForTesting final RemoteCallback mNavigationObserver = new RemoteCallback( @@ -179,6 +208,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @BackNavigationInfo.BackTargetType private int mPreviousNavigationType; private Runnable mPilferPointerCallback; + private BackAnimation.TopUiRequest mRequestTopUiCallback; public BackAnimationController( @NonNull ShellInit shellInit, @@ -188,7 +218,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont Context context, @NonNull BackAnimationBackground backAnimationBackground, ShellBackAnimationRegistry shellBackAnimationRegistry, - ShellCommandHandler shellCommandHandler) { + ShellCommandHandler shellCommandHandler, + Transitions transitions, + @ShellMainThread Handler handler) { this( shellInit, shellController, @@ -199,7 +231,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont context.getContentResolver(), backAnimationBackground, shellBackAnimationRegistry, - shellCommandHandler); + shellCommandHandler, + transitions, + handler); } @VisibleForTesting @@ -213,7 +247,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont ContentResolver contentResolver, @NonNull BackAnimationBackground backAnimationBackground, ShellBackAnimationRegistry shellBackAnimationRegistry, - ShellCommandHandler shellCommandHandler) { + ShellCommandHandler shellCommandHandler, + Transitions transitions, + @NonNull @ShellMainThread Handler handler) { mShellController = shellController; mShellExecutor = shellExecutor; mActivityTaskManager = activityTaskManager; @@ -228,6 +264,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mLatencyTracker = LatencyTracker.getInstance(mContext); mShellCommandHandler = shellCommandHandler; mWindowManager = context.getSystemService(WindowManager.class); + mTransitions = transitions; + mBackTransitionHandler = new BackTransitionHandler(); + mTransitions.addHandler(mBackTransitionHandler); + mHandler = handler; updateTouchableArea(); } @@ -357,9 +397,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mPilferPointerCallback = callback; }); } + + @Override + public void setTopUiRequestCallback(TopUiRequest topUiRequest) { + mShellExecutor.execute(() -> mRequestTopUiCallback = topUiRequest); + } } - private static class IBackAnimationImpl extends IBackAnimation.Stub + private class IBackAnimationImpl extends IBackAnimation.Stub implements ExternalInterfaceBinder { private BackAnimationController mController; @@ -377,7 +422,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont callback, runner, controller.mContext, - CUJ_PREDICTIVE_BACK_HOME))); + CUJ_PREDICTIVE_BACK_HOME, + mHandler))); } @Override @@ -422,6 +468,12 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @VisibleForTesting public void onThresholdCrossed() { mThresholdCrossed = true; + // There was no focus window when calling startBackNavigation, still pilfer pointers so + // the next focus window won't receive motion events. + if (mBackNavigationInfo == null && mReceivedNullNavigationInfo) { + tryPilferPointers(); + return; + } // Dispatch onBackStarted, only to app callbacks. // System callbacks will receive onBackStarted when the remote animation starts. final boolean shouldDispatchToAnimator = shouldDispatchToAnimator(); @@ -527,8 +579,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private void startBackNavigation(@NonNull BackTouchTracker touchTracker) { try { startLatencyTracking(); + final BackAnimationAdapter adapter = mEnableAnimations.get() + ? mBackAnimationAdapter : null; + if (adapter != null && mShellBackAnimationRegistry.hasSupportedAnimatorsChanged()) { + adapter.updateSupportedAnimators( + mShellBackAnimationRegistry.getSupportedAnimators()); + } mBackNavigationInfo = mActivityTaskManager.startBackNavigation( - mNavigationObserver, mEnableAnimations.get() ? mBackAnimationAdapter : null); + mNavigationObserver, adapter); onBackNavigationInfoReceived(mBackNavigationInfo, touchTracker); } catch (RemoteException remoteException) { Log.e(TAG, "Failed to initAnimation", remoteException); @@ -541,7 +599,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Received backNavigationInfo:%s", backNavigationInfo); if (backNavigationInfo == null) { ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Received BackNavigationInfo is null."); + mReceivedNullNavigationInfo = true; cancelLatencyTracking(); + tryPilferPointers(); return; } final int backType = backNavigationInfo.getType(); @@ -550,6 +610,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (!mShellBackAnimationRegistry.startGesture(backType)) { mActiveCallback = null; } + requestTopUi(true, backType); tryPilferPointers(); } else { mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback(); @@ -714,7 +775,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mBackAnimationFinishedCallback = null; } - if (mBackNavigationInfo != null) { + if (mBackNavigationInfo != null && !mRealCallbackInvoked) { final IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback(); if (touchTracker.getTriggerBack()) { dispatchOnBackInvoked(callback); @@ -722,6 +783,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont tryDispatchOnBackCancelled(callback); } } + mRealCallbackInvoked = false; finishBackNavigation(touchTracker.getTriggerBack()); } @@ -798,15 +860,40 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mShellExecutor.executeDelayed(mAnimationTimeoutRunnable, MAX_ANIMATION_DURATION); // The next callback should be {@link #onBackAnimationFinished}. + final boolean migrateBackToTransition = migratePredictiveBackTransition(); if (mCurrentTracker.getTriggerBack()) { - // notify gesture finished - mBackNavigationInfo.onBackGestureFinished(true); + if (migrateBackToTransition) { + // notify core gesture is commit + if (shouldTriggerCloseTransition()) { + mBackTransitionHandler.mCloseTransitionRequested = true; + final IOnBackInvokedCallback callback = + mBackNavigationInfo.getOnBackInvokedCallback(); + // invoked client side onBackInvoked + dispatchOnBackInvoked(callback); + mRealCallbackInvoked = true; + } + } else { + // notify gesture finished + mBackNavigationInfo.onBackGestureFinished(true); + } + + // start post animation dispatchOnBackInvoked(mActiveCallback); } else { tryDispatchOnBackCancelled(mActiveCallback); } } + // Close window won't create any transition + private boolean shouldTriggerCloseTransition() { + if (mBackNavigationInfo == null) { + return false; + } + int type = mBackNavigationInfo.getType(); + return type == BackNavigationInfo.TYPE_RETURN_TO_HOME + || type == BackNavigationInfo.TYPE_CROSS_TASK + || type == BackNavigationInfo.TYPE_CROSS_ACTIVITY; + } /** * Called when the post commit animation is completed or timeout. * This will trigger the real {@link IOnBackInvokedCallback} behavior. @@ -841,6 +928,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont "mCurrentBackGestureInfo was null when back animation finished"); } resetTouchTracker(); + mBackTransitionHandler.onAnimationFinished(); } /** @@ -895,10 +983,12 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mPointersPilfered = false; mShellBackAnimationRegistry.resetDefaultCrossActivity(); cancelLatencyTracking(); + mReceivedNullNavigationInfo = false; if (mBackNavigationInfo != null) { mPreviousNavigationType = mBackNavigationInfo.getType(); mBackNavigationInfo.onBackNavigationFinished(triggerBack); mBackNavigationInfo = null; + requestTopUi(false, mPreviousNavigationType); } } @@ -962,6 +1052,13 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } + private void requestTopUi(boolean hasTopUi, int backType) { + if (mRequestTopUiCallback != null && (backType == BackNavigationInfo.TYPE_CROSS_TASK + || backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY)) { + mRequestTopUiCallback.requestTopUi(hasTopUi, TAG); + } + } + /** * Validate animation targets. */ @@ -977,14 +1074,30 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return true; } + private void kickStartAnimation() { + startSystemAnimation(); + + // Dispatch the first progress after animation start for + // smoothing the initial animation, instead of waiting for next + // onMove. + final BackMotionEvent backFinish = mCurrentTracker + .createProgressEvent(); + dispatchOnBackProgressed(mActiveCallback, backFinish); + if (!mBackGestureStarted) { + // if the down -> up gesture happened before animation + // start, we have to trigger the uninterruptible transition + // to finish the back animation. + startPostCommitAnimation(); + } + } + private void createAdapter() { IBackAnimationRunner runner = new IBackAnimationRunner.Stub() { @Override public void onAnimationStart( RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, - RemoteAnimationTarget[] nonApps, + IBinder token, IBackAnimationFinishedCallback finishedCallback) { mShellExecutor.execute( () -> { @@ -995,20 +1108,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } mBackAnimationFinishedCallback = finishedCallback; mApps = apps; - startSystemAnimation(); - - // Dispatch the first progress after animation start for - // smoothing the initial animation, instead of waiting for next - // onMove. - final BackMotionEvent backFinish = mCurrentTracker - .createProgressEvent(); - dispatchOnBackProgressed(mActiveCallback, backFinish); - if (!mBackGestureStarted) { - // if the down -> up gesture happened before animation - // start, we have to trigger the uninterruptible transition - // to finish the back animation. - startPostCommitAnimation(); + // app only visible after transition ready, break for now. + if (token != null) { + return; } + kickStartAnimation(); }); } @@ -1048,4 +1152,506 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mQueuedTracker.dump(pw, prefix + " "); } + class BackTransitionHandler implements Transitions.TransitionHandler { + + Runnable mOnAnimationFinishCallback; + boolean mCloseTransitionRequested; + SurfaceControl.Transaction mFinishOpenTransaction; + Transitions.TransitionFinishCallback mFinishOpenTransitionCallback; + // The Transition to make behindActivity become visible + IBinder mPrepareOpenTransition; + // The Transition to make behindActivity become invisible, if prepare open exist and + // animation is canceled, start a close prepare transition to finish the whole transition. + IBinder mClosePrepareTransition; + TransitionInfo mOpenTransitionInfo; + void onAnimationFinished() { + if (!mCloseTransitionRequested && mPrepareOpenTransition != null) { + createClosePrepareTransition(); + } + if (mOnAnimationFinishCallback != null) { + mOnAnimationFinishCallback.run(); + mOnAnimationFinishCallback = null; + } + } + + private void applyFinishOpenTransition() { + mOpenTransitionInfo = null; + mPrepareOpenTransition = null; + if (mFinishOpenTransaction != null) { + final SurfaceControl.Transaction t = mFinishOpenTransaction; + mFinishOpenTransaction = null; + t.apply(); + } + if (mFinishOpenTransitionCallback != null) { + final Transitions.TransitionFinishCallback callback = mFinishOpenTransitionCallback; + mFinishOpenTransitionCallback = null; + callback.onTransitionFinished(null); + } + } + + private void applyAndFinish(@NonNull SurfaceControl.Transaction st, + @NonNull SurfaceControl.Transaction ft, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + applyFinishOpenTransition(); + st.apply(); + ft.apply(); + finishCallback.onTransitionFinished(null); + mCloseTransitionRequested = false; + } + @Override + public boolean startAnimation(@NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction st, + @NonNull SurfaceControl.Transaction ft, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + final boolean isPrepareTransition = + info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; + if (isPrepareTransition) { + kickStartAnimation(); + } + // Both mShellExecutor and Transitions#mMainExecutor are ShellMainThread, so we don't + // need to post to ShellExecutor when called. + if (info.getType() == WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION) { + // only consume it if this transition hasn't being processed. + if (mClosePrepareTransition != null) { + mClosePrepareTransition = null; + applyAndFinish(st, ft, finishCallback); + return true; + } + return false; + } + + if (info.getType() != WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION + && isNotGestureBackTransition(info)) { + return false; + } + + if (shouldCancelAnimation(info)) { + return false; + } + + if (mApps == null || mApps.length == 0) { + if (mCloseTransitionRequested) { + // animation never start, consume directly + applyAndFinish(st, ft, finishCallback); + return true; + } else if (mClosePrepareTransition == null && isPrepareTransition) { + // Gesture animation was cancelled before prepare transition ready, create + // the close prepare transition + createClosePrepareTransition(); + } + } + + if (handlePrepareTransition(info, st, ft, finishCallback)) { + return true; + } + return handleCloseTransition(info, st, ft, finishCallback); + } + + void createClosePrepareTransition() { + if (mClosePrepareTransition != null) { + Log.e(TAG, "Re-create close prepare transition"); + return; + } + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.restoreBackNavi(); + mClosePrepareTransition = mTransitions.startTransition( + TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION, wct, mBackTransitionHandler); + } + private void mergePendingTransitions(TransitionInfo info) { + if (mOpenTransitionInfo == null) { + return; + } + // Copy initial changes to final transition + final TransitionInfo init = mOpenTransitionInfo; + // find prepare open target + boolean openShowWallpaper = false; + ComponentName openComponent = null; + int tmpSize; + int openTaskId = INVALID_TASK_ID; + WindowContainerToken openToken = null; + for (int j = init.getChanges().size() - 1; j >= 0; --j) { + final TransitionInfo.Change change = init.getChanges().get(j); + if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) { + openComponent = findComponentName(change); + openTaskId = findTaskId(change); + openToken = findToken(change); + if (change.hasFlags(FLAG_SHOW_WALLPAPER)) { + openShowWallpaper = true; + } + break; + } + } + if (openComponent == null && openTaskId == INVALID_TASK_ID && openToken == null) { + // This shouldn't happen, but if that happen, consume the initial transition anyway. + Log.e(TAG, "Unable to merge following transition, cannot find the gesture " + + "animated target from the open transition=" + mOpenTransitionInfo); + mOpenTransitionInfo = null; + return; + } + // find first non-prepare open target + boolean isOpen = false; + tmpSize = info.getChanges().size(); + for (int j = 0; j < tmpSize; ++j) { + final TransitionInfo.Change change = info.getChanges().get(j); + final ComponentName firstNonOpen = findComponentName(change); + final int firstTaskId = findTaskId(change); + if ((firstNonOpen != null && firstNonOpen != openComponent) + || (firstTaskId != INVALID_TASK_ID && firstTaskId != openTaskId)) { + // this is original close target, potential be close, but cannot determine from + // it + if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) { + isOpen = !TransitionUtil.isClosingMode(change.getMode()); + } else { + isOpen = TransitionUtil.isOpeningMode(change.getMode()); + break; + } + } + } + + if (!isOpen) { + // Close transition, the transition info should be: + // init info(open A & wallpaper) + // current info(close B target) + // remove init info(open/change A target & wallpaper) + boolean moveToTop = false; + for (int j = info.getChanges().size() - 1; j >= 0; --j) { + final TransitionInfo.Change change = info.getChanges().get(j); + if (isSameChangeTarget(openComponent, openTaskId, openToken, change)) { + moveToTop = change.hasFlags(FLAG_MOVED_TO_TOP); + info.getChanges().remove(j); + } else if ((openShowWallpaper && change.hasFlags(FLAG_IS_WALLPAPER)) + || !change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) { + info.getChanges().remove(j); + } + } + // Ignore merge if there is no close target + if (!info.getChanges().isEmpty()) { + tmpSize = init.getChanges().size(); + for (int i = 0; i < tmpSize; ++i) { + final TransitionInfo.Change change = init.getChanges().get(i); + if (change.hasFlags(FLAG_IS_WALLPAPER)) { + continue; + } + if (moveToTop) { + if (isSameChangeTarget(openComponent, openTaskId, openToken, change)) { + change.setFlags(change.getFlags() | FLAG_MOVED_TO_TOP); + } + } + info.getChanges().add(i, change); + } + } + } else { + // Open transition, the transition info should be: + // init info(open A & wallpaper) + // current info(open C target + close B target + close A & wallpaper) + + // If close target isn't back navigated, filter out close A & wallpaper because the + // (open C + close B) pair didn't participant prepare close + boolean nonBackOpen = false; + boolean nonBackClose = false; + tmpSize = info.getChanges().size(); + for (int j = 0; j < tmpSize; ++j) { + final TransitionInfo.Change change = info.getChanges().get(j); + if (!change.hasFlags(FLAG_BACK_GESTURE_ANIMATED) + && canBeTransitionTarget(change)) { + final int mode = change.getMode(); + nonBackOpen |= TransitionUtil.isOpeningMode(mode); + nonBackClose |= TransitionUtil.isClosingMode(mode); + } + } + if (nonBackClose && nonBackOpen) { + for (int j = info.getChanges().size() - 1; j >= 0; --j) { + final TransitionInfo.Change change = info.getChanges().get(j); + if (isSameChangeTarget(openComponent, openTaskId, openToken, change)) { + info.getChanges().remove(j); + } else if ((openShowWallpaper && change.hasFlags(FLAG_IS_WALLPAPER))) { + info.getChanges().remove(j); + } + } + } + } + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation transition, merge pending " + + "transitions result=%s", info); + // Only handle one merge transition request. + mOpenTransitionInfo = null; + } + + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (mClosePrepareTransition == transition) { + mClosePrepareTransition = null; + } + // try to handle unexpected transition + if (mOpenTransitionInfo != null) { + mergePendingTransitions(info); + } + + if (info.getType() == TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION + && !mCloseTransitionRequested && info.getChanges().isEmpty() && mApps == null) { + finishCallback.onTransitionFinished(null); + t.apply(); + applyFinishOpenTransition(); + return; + } + if (isNotGestureBackTransition(info) || shouldCancelAnimation(info) + || !mCloseTransitionRequested) { + if (mPrepareOpenTransition != null) { + applyFinishOpenTransition(); + } + return; + } + // Handle the commit transition if this handler is running the open transition. + finishCallback.onTransitionFinished(null); + t.apply(); + if (mCloseTransitionRequested) { + if (mApps == null || mApps.length == 0) { + // animation was done + applyFinishOpenTransition(); + mCloseTransitionRequested = false; + } else { + // we are animating, wait until animation finish + mOnAnimationFinishCallback = () -> { + applyFinishOpenTransition(); + mCloseTransitionRequested = false; + }; + } + } + } + + // Cancel close animation if something happen unexpected, let another handler to handle + private boolean shouldCancelAnimation(@NonNull TransitionInfo info) { + final boolean noCloseAllowed = !mCloseTransitionRequested + && info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; + boolean unableToHandle = false; + boolean filterTargets = false; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change c = info.getChanges().get(i); + final boolean backGestureAnimated = c.hasFlags(FLAG_BACK_GESTURE_ANIMATED); + if (!backGestureAnimated && !c.hasFlags(FLAG_IS_WALLPAPER)) { + // something we cannot handle? + unableToHandle = true; + filterTargets = true; + } else if (noCloseAllowed && backGestureAnimated + && TransitionUtil.isClosingMode(c.getMode())) { + // Prepare back navigation shouldn't contain close change, unless top app + // request close. + unableToHandle = true; + } + } + if (!unableToHandle) { + return false; + } + if (!filterTargets) { + return true; + } + if (TransitionUtil.isOpeningType(info.getType()) + || TransitionUtil.isClosingType(info.getType())) { + boolean removeWallpaper = false; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change c = info.getChanges().get(i); + // filter out opening target, keep original closing target in this transition + if (c.hasFlags(FLAG_BACK_GESTURE_ANIMATED) + && TransitionUtil.isOpeningMode(c.getMode())) { + info.getChanges().remove(i); + removeWallpaper |= c.hasFlags(FLAG_SHOW_WALLPAPER); + } + } + if (removeWallpaper) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change c = info.getChanges().get(i); + if (c.hasFlags(FLAG_IS_WALLPAPER)) { + info.getChanges().remove(i); + } + } + } + } + return true; + } + + /** + * Check whether this transition is prepare for predictive back animation, which could + * happen when core make an activity become visible. + */ + @VisibleForTesting + boolean handlePrepareTransition( + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction st, + @NonNull SurfaceControl.Transaction ft, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (info.getType() != WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) { + return false; + } + // Must have open target, must not have close target. + if (hasAnimationInMode(info, TransitionUtil::isClosingMode) + || !hasAnimationInMode(info, TransitionUtil::isOpeningMode)) { + return false; + } + SurfaceControl openingLeash = null; + if (mApps != null) { + for (int i = mApps.length - 1; i >= 0; --i) { + if (mApps[i].mode == MODE_OPENING) { + openingLeash = mApps[i].leash; + } + } + } + if (openingLeash != null) { + int rootIdx = -1; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change c = info.getChanges().get(i); + if (TransitionUtil.isOpeningMode(c.getMode())) { + final Point offset = c.getEndRelOffset(); + st.setPosition(c.getLeash(), offset.x, offset.y); + st.reparent(c.getLeash(), openingLeash); + st.setAlpha(c.getLeash(), 1.0f); + rootIdx = TransitionUtil.rootIndexFor(c, info); + } + } + // The root leash and the leash of opening target should actually in the same level, + // but since the root leash is created after opening target, it will have higher + // layer in surface flinger. Move the root leash to lower level, so it won't affect + // the playing animation. + if (rootIdx >= 0 && info.getRootCount() > 0) { + st.setLayer(info.getRoot(rootIdx).getLeash(), -1); + } + } + st.apply(); + mFinishOpenTransaction = ft; + mFinishOpenTransitionCallback = finishCallback; + mOpenTransitionInfo = info; + return true; + } + + /** + * Check whether this transition is triggered from back gesture commitment. + * Reparent the transition targets to animation leashes, so the animation won't be broken. + */ + @VisibleForTesting + boolean handleCloseTransition(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction st, + @NonNull SurfaceControl.Transaction ft, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (!mCloseTransitionRequested) { + return false; + } + // must have close target + if (!hasAnimationInMode(info, TransitionUtil::isClosingMode)) { + return false; + } + if (mApps == null) { + // animation is done + applyAndFinish(st, ft, finishCallback); + return true; + } + SurfaceControl openingLeash = null; + SurfaceControl closingLeash = null; + for (int i = mApps.length - 1; i >= 0; --i) { + if (mApps[i].mode == MODE_OPENING) { + openingLeash = mApps[i].leash; + } + if (mApps[i].mode == MODE_CLOSING) { + closingLeash = mApps[i].leash; + } + } + if (openingLeash != null && closingLeash != null) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change c = info.getChanges().get(i); + if (c.hasFlags(FLAG_IS_WALLPAPER)) { + st.setAlpha(c.getLeash(), 1.0f); + continue; + } + if (TransitionUtil.isOpeningMode(c.getMode())) { + final Point offset = c.getEndRelOffset(); + st.setPosition(c.getLeash(), offset.x, offset.y); + st.reparent(c.getLeash(), openingLeash); + st.setAlpha(c.getLeash(), 1.0f); + } else if (TransitionUtil.isClosingMode(c.getMode())) { + st.reparent(c.getLeash(), closingLeash); + } + } + } + st.apply(); + // mApps must exists + mOnAnimationFinishCallback = () -> { + ft.apply(); + finishCallback.onTransitionFinished(null); + mCloseTransitionRequested = false; + }; + return true; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest( + @NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + final int type = request.getType(); + if (type == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) { + mPrepareOpenTransition = transition; + return new WindowContainerTransaction(); + } + if (type == WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION) { + return new WindowContainerTransaction(); + } + if (TransitionUtil.isClosingType(request.getType()) && mCloseTransitionRequested) { + return new WindowContainerTransaction(); + } + return null; + } + } + + private static boolean isNotGestureBackTransition(@NonNull TransitionInfo info) { + return !hasAnimationInMode(info, TransitionUtil::isOpenOrCloseMode); + } + + private static boolean hasAnimationInMode(@NonNull TransitionInfo info, + Predicate<Integer> mode) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change c = info.getChanges().get(i); + if (c.hasFlags(FLAG_BACK_GESTURE_ANIMATED) && mode.test(c.getMode())) { + return true; + } + } + return false; + } + + private static WindowContainerToken findToken(TransitionInfo.Change change) { + return change.getContainer(); + } + + private static ComponentName findComponentName(TransitionInfo.Change change) { + final ComponentName componentName = change.getActivityComponent(); + if (componentName != null) { + return componentName; + } + final TaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo != null) { + return taskInfo.topActivity; + } + return null; + } + + private static int findTaskId(TransitionInfo.Change change) { + final TaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo != null) { + return taskInfo.taskId; + } + return INVALID_TASK_ID; + } + + private static boolean isSameChangeTarget(ComponentName topActivity, int taskId, + WindowContainerToken token, TransitionInfo.Change change) { + final ComponentName openChange = findComponentName(change); + final int firstTaskId = findTaskId(change); + final WindowContainerToken openToken = findToken(change); + return (openChange != null && openChange == topActivity) + || (firstTaskId != INVALID_TASK_ID && firstTaskId == taskId) + || (openToken != null && token == openToken); + } + + private static boolean canBeTransitionTarget(TransitionInfo.Change change) { + return findComponentName(change) != null || findTaskId(change) != INVALID_TASK_ID; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java index 851472f7d4c1..4569cf31dab1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java @@ -20,6 +20,7 @@ import static android.view.WindowManager.TRANSIT_OLD_UNSET; import android.annotation.NonNull; import android.content.Context; +import android.os.Handler; import android.os.RemoteException; import android.util.Log; import android.view.IRemoteAnimationFinishedCallback; @@ -31,7 +32,8 @@ import android.window.IOnBackInvokedCallback; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.Cuj.CujType; -import com.android.wm.shell.common.InteractionJankMonitorUtils; +import com.android.internal.jank.InteractionJankMonitor; +import com.android.wm.shell.shared.annotations.ShellMainThread; import java.lang.ref.WeakReference; @@ -48,6 +50,8 @@ public class BackAnimationRunner { private final IRemoteAnimationRunner mRunner; private final @CujType int mCujType; private final Context mContext; + @ShellMainThread + private final Handler mHandler; // Whether we are waiting to receive onAnimationStart private boolean mWaitingAnimation; @@ -59,18 +63,35 @@ public class BackAnimationRunner { @NonNull IOnBackInvokedCallback callback, @NonNull IRemoteAnimationRunner runner, @NonNull Context context, - @CujType int cujType) { + @CujType int cujType, + @ShellMainThread Handler handler) { mCallback = callback; mRunner = runner; mCujType = cujType; mContext = context; + mHandler = handler; } public BackAnimationRunner( @NonNull IOnBackInvokedCallback callback, @NonNull IRemoteAnimationRunner runner, - @NonNull Context context) { - this(callback, runner, context, NO_CUJ); + @NonNull Context context, + @ShellMainThread Handler handler + ) { + this(callback, runner, context, NO_CUJ, handler); + } + + /** + * @deprecated Use {@link BackAnimationRunner} constructor providing an handler for the ui + * thread of the animation. + */ + @Deprecated + public BackAnimationRunner( + @NonNull IOnBackInvokedCallback callback, + @NonNull IRemoteAnimationRunner runner, + @NonNull Context context + ) { + this(callback, runner, context, NO_CUJ, context.getMainThreadHandler()); } /** Returns the registered animation runner */ @@ -102,7 +123,7 @@ public class BackAnimationRunner { return; } if (runner.shouldMonitorCUJ(runner.mApps)) { - InteractionJankMonitorUtils.endTracing(runner.mCujType); + InteractionJankMonitor.getInstance().end(runner.mCujType); } runner.mFinishedCallback.run(); @@ -123,13 +144,14 @@ public class BackAnimationRunner { */ void startAnimation(RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, Runnable finishedCallback) { + InteractionJankMonitor interactionJankMonitor = InteractionJankMonitor.getInstance(); mFinishedCallback = finishedCallback; mApps = apps; if (mRemoteCallback == null) mRemoteCallback = new RemoteAnimationFinishedStub(this); mWaitingAnimation = false; if (shouldMonitorCUJ(apps)) { - InteractionJankMonitorUtils.beginTracing( - mCujType, mContext, apps[0].leash, /* tag */ null); + interactionJankMonitor.begin( + apps[0].leash, mContext, mHandler, mCujType); } try { getRunner().onAnimationStart(TRANSIT_OLD_UNSET, apps, wallpapers, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt index 169e122c353c..37339307f5b0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt @@ -26,6 +26,7 @@ import android.graphics.Matrix import android.graphics.PointF import android.graphics.Rect import android.graphics.RectF +import android.os.Handler import android.os.RemoteException import android.util.TimeUtils import android.view.Choreographer @@ -48,11 +49,12 @@ import com.android.internal.dynamicanimation.animation.SpringForce import com.android.internal.jank.Cuj import com.android.internal.policy.ScreenDecorationsUtils import com.android.internal.policy.SystemBarUtils -import com.android.internal.protolog.common.ProtoLog +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer -import com.android.wm.shell.animation.Interpolators import com.android.wm.shell.protolog.ShellProtoLogGroup +import com.android.wm.shell.shared.animation.Interpolators +import com.android.wm.shell.shared.annotations.ShellMainThread import kotlin.math.abs import kotlin.math.max import kotlin.math.min @@ -61,7 +63,8 @@ abstract class CrossActivityBackAnimation( private val context: Context, private val background: BackAnimationBackground, private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, - protected val transaction: SurfaceControl.Transaction + protected val transaction: SurfaceControl.Transaction, + @ShellMainThread handler: Handler, ) : ShellBackAnimation() { protected val startClosingRect = RectF() @@ -80,7 +83,13 @@ abstract class CrossActivityBackAnimation( private var statusbarHeight = SystemBarUtils.getStatusBarHeight(context) private val backAnimationRunner = - BackAnimationRunner(Callback(), Runner(), context, Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY) + BackAnimationRunner( + Callback(), + Runner(), + context, + Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY, + handler, + ) private val initialTouchPos = PointF() private val transformMatrix = Matrix() private val tmpFloat9 = FloatArray(9) @@ -170,7 +179,7 @@ abstract class CrossActivityBackAnimation( initialTouchPos.set(backMotionEvent.touchX, backMotionEvent.touchY) transaction.setAnimationTransaction() - isLetterboxed = closingTarget!!.taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed + isLetterboxed = closingTarget!!.taskInfo.appCompatTaskInfo.isTopActivityLetterboxed enteringHasSameLetterbox = isLetterboxed && closingTarget!!.localBounds.equals(enteringTarget!!.localBounds) @@ -189,10 +198,13 @@ abstract class CrossActivityBackAnimation( preparePreCommitEnteringRectMovement() background.ensureBackground( - closingTarget!!.windowConfiguration.bounds, - getBackgroundColor(), - transaction, - statusbarHeight + closingTarget!!.windowConfiguration.bounds, + getBackgroundColor(), + transaction, + statusbarHeight, + if (closingTarget!!.windowConfiguration.tasksAreFloating()) + closingTarget!!.localBounds else null, + cornerRadius ) ensureScrimLayer() if (isLetterboxed && enteringHasSameLetterbox) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java index 381914a58cf2..7a569799ab84 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java @@ -34,6 +34,7 @@ import android.graphics.Matrix; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; +import android.os.Handler; import android.os.RemoteException; import android.view.Choreographer; import android.view.IRemoteAnimationFinishedCallback; @@ -49,9 +50,9 @@ import android.window.IOnBackInvokedCallback; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.policy.SystemBarUtils; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; -import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.shared.animation.Interpolators; import com.android.wm.shell.shared.annotations.ShellMainThread; import javax.inject.Inject; @@ -69,7 +70,6 @@ import javax.inject.Inject; * IOnBackInvokedCallback} with WM Shell and receives back dispatches when a back navigation to * launcher starts. */ -@ShellMainThread public class CrossTaskBackAnimation extends ShellBackAnimation { private static final int BACKGROUNDCOLOR = 0x43433A; @@ -115,9 +115,10 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { private float mVerticalMargin; @Inject - public CrossTaskBackAnimation(Context context, BackAnimationBackground background) { + public CrossTaskBackAnimation(Context context, BackAnimationBackground background, + @ShellMainThread Handler handler) { mBackAnimationRunner = new BackAnimationRunner( - new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_TASK); + new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_TASK, handler); mBackground = background; mContext = context; loadResources(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt index 9ebab6383416..2f7666b21882 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.back import android.content.Context import android.graphics.Rect import android.graphics.RectF +import android.os.Handler import android.util.MathUtils import android.view.SurfaceControl import android.view.animation.Animation @@ -27,9 +28,10 @@ import android.window.BackMotionEvent import android.window.BackNavigationInfo import com.android.internal.R import com.android.internal.policy.TransitionAnimation -import com.android.internal.protolog.common.ProtoLog +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.protolog.ShellProtoLogGroup +import com.android.wm.shell.shared.annotations.ShellMainThread import javax.inject.Inject import kotlin.math.max import kotlin.math.min @@ -40,13 +42,15 @@ class CustomCrossActivityBackAnimation( background: BackAnimationBackground, rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, transaction: SurfaceControl.Transaction, - private val customAnimationLoader: CustomAnimationLoader + private val customAnimationLoader: CustomAnimationLoader, + @ShellMainThread handler: Handler, ) : CrossActivityBackAnimation( context, background, rootTaskDisplayAreaOrganizer, - transaction + transaction, + handler ) { private var enterAnimation: Animation? = null @@ -59,7 +63,8 @@ class CustomCrossActivityBackAnimation( constructor( context: Context, background: BackAnimationBackground, - rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer + rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + @ShellMainThread handler: Handler, ) : this( context, background, @@ -67,7 +72,8 @@ class CustomCrossActivityBackAnimation( SurfaceControl.Transaction(), CustomAnimationLoader( TransitionAnimation(context, false /* debug */, "CustomCrossActivityBackAnimation") - ) + ), + handler, ) override fun preparePreCommitClosingRectMovement(swipeEdge: Int) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt index c747e1e98956..eecd7694009d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt @@ -16,11 +16,13 @@ package com.android.wm.shell.back import android.content.Context +import android.os.Handler import android.view.SurfaceControl import android.window.BackEvent import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer -import com.android.wm.shell.animation.Interpolators +import com.android.wm.shell.shared.animation.Interpolators +import com.android.wm.shell.shared.annotations.ShellMainThread import javax.inject.Inject import kotlin.math.max @@ -30,13 +32,15 @@ class DefaultCrossActivityBackAnimation constructor( context: Context, background: BackAnimationBackground, - rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer + rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + @ShellMainThread handler: Handler, ) : CrossActivityBackAnimation( context, background, rootTaskDisplayAreaOrganizer, - SurfaceControl.Transaction() + SurfaceControl.Transaction(), + handler ) { private val postCommitInterpolator = Interpolators.EMPHASIZED diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimationRegistry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimationRegistry.java index 6fafa75e2f70..ae2c7b3adb6b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimationRegistry.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimationRegistry.java @@ -23,6 +23,8 @@ import android.util.Log; import android.util.SparseArray; import android.window.BackNavigationInfo; +import java.util.ArrayList; + /** Registry for all types of default back animations */ public class ShellBackAnimationRegistry { private static final String TAG = "ShellBackPreview"; @@ -31,6 +33,8 @@ public class ShellBackAnimationRegistry { private ShellBackAnimation mDefaultCrossActivityAnimation; private final ShellBackAnimation mCustomizeActivityAnimation; private final ShellBackAnimation mCrossTaskAnimation; + private boolean mSupportedAnimatorsChanged = false; + private final ArrayList<Integer> mSupportedAnimators = new ArrayList<>(); public ShellBackAnimationRegistry( @ShellBackAnimation.CrossActivity @Nullable ShellBackAnimation crossActivityAnimation, @@ -60,7 +64,7 @@ public class ShellBackAnimationRegistry { mDefaultCrossActivityAnimation = crossActivityAnimation; mCustomizeActivityAnimation = customizeActivityAnimation; mCrossTaskAnimation = crossTaskAnimation; - + updateSupportedAnimators(); // TODO(b/236760237): register dialog close animation when it's completed. } @@ -71,6 +75,7 @@ public class ShellBackAnimationRegistry { if (BackNavigationInfo.TYPE_CROSS_ACTIVITY == type) { mDefaultCrossActivityAnimation = null; } + updateSupportedAnimators(); } void unregisterAnimation(@BackNavigationInfo.BackTargetType int type) { @@ -79,6 +84,24 @@ public class ShellBackAnimationRegistry { if (BackNavigationInfo.TYPE_CROSS_ACTIVITY == type) { mDefaultCrossActivityAnimation = null; } + updateSupportedAnimators(); + } + + private void updateSupportedAnimators() { + mSupportedAnimators.clear(); + for (int i = mAnimationDefinition.size() - 1; i >= 0; --i) { + mSupportedAnimators.add(mAnimationDefinition.keyAt(i)); + } + mSupportedAnimatorsChanged = true; + } + + boolean hasSupportedAnimatorsChanged() { + return mSupportedAnimatorsChanged; + } + + ArrayList<Integer> getSupportedAnimators() { + mSupportedAnimatorsChanged = false; + return mSupportedAnimators; } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java index f9a1d940c734..c1dadada505a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java @@ -37,7 +37,7 @@ import androidx.constraintlayout.widget.ConstraintLayout; import com.android.launcher3.icons.DotRenderer; import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.R; -import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.shared.animation.Interpolators; import java.util.EnumSet; @@ -357,7 +357,9 @@ public class BadgedImageView extends ConstraintLayout { void showBadge() { Bitmap appBadgeBitmap = mBubble.getAppBadge(); - if (appBadgeBitmap == null) { + final boolean isAppLaunchIntent = (mBubble instanceof Bubble) + && ((Bubble) mBubble).isAppLaunchIntent(); + if (appBadgeBitmap == null || isAppLaunchIntent) { mAppIcon.setVisibility(GONE); return; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index 1279fc42c066..169361ad5f6b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -48,11 +48,14 @@ import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.BubbleIconFactory; +import com.android.wm.shell.Flags; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; -import com.android.wm.shell.common.bubbles.BubbleInfo; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.bubbles.BubbleInfo; import java.io.PrintWriter; import java.util.List; @@ -78,6 +81,7 @@ public class Bubble implements BubbleViewProvider { private final LocusId mLocusId; private final Executor mMainExecutor; + private final Executor mBgExecutor; private long mLastUpdated; private long mLastAccessed; @@ -110,7 +114,10 @@ public class Bubble implements BubbleViewProvider { @Nullable private BubbleTaskView mBubbleTaskView; + @Nullable private BubbleViewInfoTask mInflationTask; + @Nullable + private BubbleViewInfoTaskLegacy mInflationTaskLegacy; private boolean mInflateSynchronously; private boolean mPendingIntentCanceled; private boolean mIsImportantConversation; @@ -202,7 +209,9 @@ public class Bubble implements BubbleViewProvider { @VisibleForTesting(visibility = PRIVATE) public Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo, final int desiredHeight, final int desiredHeightResId, @Nullable final String title, - int taskId, @Nullable final String locus, boolean isDismissable, Executor mainExecutor, + int taskId, @Nullable final String locus, boolean isDismissable, + @ShellMainThread Executor mainExecutor, + @ShellBackgroundThread Executor bgExecutor, final Bubbles.BubbleMetadataFlagListener listener) { Objects.requireNonNull(key); Objects.requireNonNull(shortcutInfo); @@ -221,6 +230,7 @@ public class Bubble implements BubbleViewProvider { mTitle = title; mShowBubbleUpdateDot = false; mMainExecutor = mainExecutor; + mBgExecutor = bgExecutor; mTaskId = taskId; mBubbleMetadataFlagListener = listener; mIsAppBubble = false; @@ -232,7 +242,8 @@ public class Bubble implements BubbleViewProvider { @Nullable Icon icon, boolean isAppBubble, String key, - Executor mainExecutor) { + @ShellMainThread Executor mainExecutor, + @ShellBackgroundThread Executor bgExecutor) { mGroupKey = null; mLocusId = null; mFlags = 0; @@ -242,25 +253,48 @@ public class Bubble implements BubbleViewProvider { mKey = key; mShowBubbleUpdateDot = false; mMainExecutor = mainExecutor; + mBgExecutor = bgExecutor; mTaskId = INVALID_TASK_ID; mAppIntent = intent; mDesiredHeight = Integer.MAX_VALUE; mPackageName = intent.getPackage(); + } + private Bubble(ShortcutInfo info, @ShellMainThread Executor mainExecutor, + @ShellBackgroundThread Executor bgExecutor) { + mGroupKey = null; + mLocusId = null; + mFlags = 0; + mUser = info.getUserHandle(); + mIcon = info.getIcon(); + mIsAppBubble = false; + mKey = getBubbleKeyForShortcut(info); + mShowBubbleUpdateDot = false; + mMainExecutor = mainExecutor; + mBgExecutor = bgExecutor; + mTaskId = INVALID_TASK_ID; + mAppIntent = null; + mDesiredHeight = Integer.MAX_VALUE; + mPackageName = info.getPackage(); + mShortcutInfo = info; } /** Creates an app bubble. */ - public static Bubble createAppBubble( - Intent intent, - UserHandle user, - @Nullable Icon icon, - Executor mainExecutor) { + public static Bubble createAppBubble(Intent intent, UserHandle user, @Nullable Icon icon, + @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { return new Bubble(intent, user, icon, /* isAppBubble= */ true, /* key= */ getAppBubbleKeyForApp(intent.getPackage(), user), - mainExecutor); + mainExecutor, bgExecutor); + } + + /** Creates a shortcut bubble. */ + public static Bubble createShortcutBubble( + ShortcutInfo info, + @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { + return new Bubble(info, mainExecutor, bgExecutor); } /** @@ -273,11 +307,19 @@ public class Bubble implements BubbleViewProvider { return KEY_APP_BUBBLE + ":" + user.getIdentifier() + ":" + packageName; } + /** + * Returns the key for a shortcut bubble using {@code packageName}, {@code user}, and the + * {@code shortcutInfo} id. + */ + public static String getBubbleKeyForShortcut(ShortcutInfo info) { + return info.getPackage() + ":" + info.getUserId() + ":" + info.getId(); + } + @VisibleForTesting(visibility = PRIVATE) public Bubble(@NonNull final BubbleEntry entry, final Bubbles.BubbleMetadataFlagListener listener, final Bubbles.PendingIntentCanceledListener intentCancelListener, - Executor mainExecutor) { + @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { mIsAppBubble = false; mKey = entry.getKey(); mGroupKey = entry.getGroupKey(); @@ -292,6 +334,7 @@ public class Bubble implements BubbleViewProvider { }); }; mMainExecutor = mainExecutor; + mBgExecutor = bgExecutor; mTaskId = INVALID_TASK_ID; setEntry(entry); } @@ -306,7 +349,8 @@ public class Bubble implements BubbleViewProvider { getPackageName(), getTitle(), getAppName(), - isImportantConversation()); + isImportantConversation(), + !isAppLaunchIntent()); } @Override @@ -525,40 +569,72 @@ public class Bubble implements BubbleViewProvider { @Nullable BubbleBarLayerView layerView, BubbleIconFactory iconFactory, boolean skipInflation) { - if (isBubbleLoading()) { - mInflationTask.cancel(true /* mayInterruptIfRunning */); - } - mInflationTask = new BubbleViewInfoTask(this, - context, - expandedViewManager, - taskViewFactory, - positioner, - stackView, - layerView, - iconFactory, - skipInflation, - callback, - mMainExecutor); - if (mInflateSynchronously) { - mInflationTask.onPostExecute(mInflationTask.doInBackground()); + ProtoLog.v(WM_SHELL_BUBBLES, "Inflate bubble key=%s", getKey()); + if (Flags.bubbleViewInfoExecutors()) { + if (mInflationTask != null && !mInflationTask.isFinished()) { + mInflationTask.cancel(); + } + mInflationTask = new BubbleViewInfoTask(this, + context, + expandedViewManager, + taskViewFactory, + positioner, + stackView, + layerView, + iconFactory, + skipInflation, + callback, + mMainExecutor, + mBgExecutor); + if (mInflateSynchronously) { + mInflationTask.startSync(); + } else { + mInflationTask.start(); + } } else { - mInflationTask.execute(); + if (mInflationTaskLegacy != null && mInflationTaskLegacy.getStatus() != FINISHED) { + mInflationTaskLegacy.cancel(true /* mayInterruptIfRunning */); + } + mInflationTaskLegacy = new BubbleViewInfoTaskLegacy(this, + context, + expandedViewManager, + taskViewFactory, + positioner, + stackView, + layerView, + iconFactory, + skipInflation, + bubble -> { + if (callback != null) { + callback.onBubbleViewsReady(bubble); + } + }, + mMainExecutor, + mBgExecutor); + if (mInflateSynchronously) { + mInflationTaskLegacy.onPostExecute(mInflationTaskLegacy.doInBackground()); + } else { + mInflationTaskLegacy.execute(); + } } } - private boolean isBubbleLoading() { - return mInflationTask != null && mInflationTask.getStatus() != FINISHED; - } - boolean isInflated() { return (mIconView != null && mExpandedView != null) || mBubbleBarExpandedView != null; } void stopInflation() { - if (mInflationTask == null) { - return; + if (Flags.bubbleViewInfoExecutors()) { + if (mInflationTask == null) { + return; + } + mInflationTask.cancel(); + } else { + if (mInflationTaskLegacy == null) { + return; + } + mInflationTaskLegacy.cancel(true /* mayInterruptIfRunning */); } - mInflationTask.cancel(true /* mayInterruptIfRunning */); } void setViewInfo(BubbleViewInfoTask.BubbleViewInfo info) { @@ -594,6 +670,42 @@ public class Bubble implements BubbleViewProvider { } /** + * @deprecated {@link BubbleViewInfoTaskLegacy} is deprecated. + */ + @Deprecated + void setViewInfoLegacy(BubbleViewInfoTaskLegacy.BubbleViewInfo info) { + if (!isInflated()) { + mIconView = info.imageView; + mExpandedView = info.expandedView; + mBubbleBarExpandedView = info.bubbleBarExpandedView; + } + + mShortcutInfo = info.shortcutInfo; + mAppName = info.appName; + if (mTitle == null) { + mTitle = mAppName; + } + mFlyoutMessage = info.flyoutMessage; + + mBadgeBitmap = info.badgeBitmap; + mRawBadgeBitmap = info.rawBadgeBitmap; + mBubbleBitmap = info.bubbleBitmap; + + mDotColor = info.dotColor; + mDotPath = info.dotPath; + + if (mExpandedView != null) { + mExpandedView.update(this /* bubble */); + } + if (mBubbleBarExpandedView != null) { + mBubbleBarExpandedView.update(this /* bubble */); + } + if (mIconView != null) { + mIconView.setRenderedBubble(this /* bubble */); + } + } + + /** * Set visibility of bubble in the expanded state. * * <p>Note that this contents visibility doesn't affect visibility at {@link android.view.View}, @@ -888,17 +1000,39 @@ public class Bubble implements BubbleViewProvider { return mIntent; } + /** + * Whether this bubble represents the full app, i.e. the intent used is the launch + * intent for an app. In this case we don't show a badge on the icon. + */ + public boolean isAppLaunchIntent() { + if (Flags.enableBubbleAnything() && mAppIntent != null) { + return mAppIntent.hasCategory("android.intent.category.LAUNCHER"); + } + return false; + } + @Nullable PendingIntent getDeleteIntent() { return mDeleteIntent; } @Nullable - Intent getAppBubbleIntent() { + @VisibleForTesting + public Intent getAppBubbleIntent() { return mAppIntent; } /** + * Sets the intent for a bubble that is an app bubble (one for which {@link #mIsAppBubble} is + * true). + * + * @param appIntent The intent to set for the app bubble. + */ + void setAppBubbleIntent(Intent appIntent) { + mAppIntent = appIntent; + } + + /** * Returns whether this bubble is from an app versus a notification. */ public boolean isAppBubble() { 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 d2c36e6b637c..af4a0c55f28d 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 @@ -35,7 +35,7 @@ import static com.android.wm.shell.bubbles.Bubbles.DISMISS_PACKAGE_REMOVED; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_SHORTCUT_REMOVED; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_CHANGED; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; -import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BUBBLES; +import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_BUBBLES; import android.annotation.BinderThread; import android.annotation.NonNull; @@ -84,7 +84,7 @@ import androidx.annotation.MainThread; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.util.CollectionUtils; import com.android.launcher3.icons.BubbleIconFactory; @@ -104,14 +104,14 @@ import com.android.wm.shell.common.SingleInstanceRemoteListener; 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.common.bubbles.BubbleBarLocation; -import com.android.wm.shell.common.bubbles.BubbleBarUpdate; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; import com.android.wm.shell.pip.PinnedStackListenerForwarder; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; @@ -734,6 +734,9 @@ public class BubbleController implements ConfigurationChangeListener, public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { if (canShowAsBubbleBar()) { mBubblePositioner.setBubbleBarLocation(bubbleBarLocation); + if (mLayerView != null && !mLayerView.isExpandedViewDragged()) { + mLayerView.updateExpandedView(); + } BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation; mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate); @@ -1222,7 +1225,7 @@ public class BubbleController implements ConfigurationChangeListener, mBubblePositioner.setBubbleBarLocation(location); mBubblePositioner.setBubbleBarTopOnScreen(topOnScreen); if (mBubbleData.getSelectedBubble() != null) { - mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ true); + showExpandedViewForBubbleBar(); } } @@ -1230,13 +1233,18 @@ public class BubbleController implements ConfigurationChangeListener, * A bubble was dragged and is released in dismiss target in Launcher. * * @param bubbleKey key of the bubble being dragged to dismiss target + * @param timestamp the timestamp of the removal */ - public void dragBubbleToDismiss(String bubbleKey) { + public void dragBubbleToDismiss(String bubbleKey, long timestamp) { String selectedBubbleKey = mBubbleData.getSelectedBubbleKey(); - removeBubble(bubbleKey, Bubbles.DISMISS_USER_GESTURE); - if (selectedBubbleKey != null && !selectedBubbleKey.equals(bubbleKey)) { - // We did not remove the selected bubble. Expand it again - mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ true); + if (mBubbleData.hasAnyBubbleWithKey(bubbleKey)) { + mBubbleData.dismissBubbleWithKey( + bubbleKey, Bubbles.DISMISS_USER_GESTURE_FROM_LAUNCHER, timestamp); + } + if (mBubbleData.hasBubbles()) { + // We still have bubbles, if we dragged an individual bubble to dismiss we were expanded + // so re-expand to whatever is selected. + showExpandedViewForBubbleBar(); } } @@ -1328,6 +1336,40 @@ public class BubbleController implements ConfigurationChangeListener, } /** + * Expands and selects a bubble created or found via the provided shortcut info. + * + * @param info the shortcut info for the bubble. + */ + public void expandStackAndSelectBubble(ShortcutInfo info) { + if (!Flags.enableBubbleAnything()) return; + Bubble b = mBubbleData.getOrCreateBubble(info); // Removes from overflow + ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - shortcut=%s", info); + if (b.isInflated()) { + mBubbleData.setSelectedBubbleAndExpandStack(b); + } else { + b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + } + } + + /** + * Expands and selects a bubble created or found for this app. + * + * @param intent the intent for the bubble. + */ + public void expandStackAndSelectBubble(Intent intent) { + if (!Flags.enableBubbleAnything()) return; + Bubble b = mBubbleData.getOrCreateBubble(intent); // Removes from overflow + ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", intent); + if (b.isInflated()) { + mBubbleData.setSelectedBubbleAndExpandStack(b); + } else { + b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + } + } + + /** * Expands and selects a bubble based on the provided {@link BubbleEntry}. If no bubble * exists for this entry, and it is able to bubble, a new bubble will be created. * @@ -1450,9 +1492,11 @@ public class BubbleController implements ConfigurationChangeListener, if (b != null) { // It's in the overflow, so remove it & reinflate mBubbleData.dismissBubbleWithKey(appBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL); + // Update the bubble entry in the overflow with the latest intent. + b.setAppBubbleIntent(intent); } else { // App bubble does not exist, lets add and expand it - b = Bubble.createAppBubble(intent, user, icon, mMainExecutor); + b = Bubble.createAppBubble(intent, user, icon, mMainExecutor, mBackgroundExecutor); } ProtoLog.d(WM_SHELL_BUBBLES, "inflateAndAdd %s", appBubbleKey); b.setShouldAutoExpand(true); @@ -1954,22 +1998,17 @@ public class BubbleController implements ConfigurationChangeListener, @Override public void expansionChanged(boolean isExpanded) { - if (mLayerView != null) { - if (!isExpanded) { - mLayerView.collapse(); - } else { - BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); - if (selectedBubble != null) { - mLayerView.showExpandedView(selectedBubble); - } - } + // in bubble bar mode, let the request to show the expanded view come from launcher. + // only collapse here if we're collapsing. + if (mLayerView != null && !isExpanded) { + mLayerView.collapse(); } } @Override public void selectionChanged(BubbleViewProvider selectedBubble) { // Only need to update the layer view if we're currently expanded for selection changes. - if (mLayerView != null && isStackExpanded()) { + if (mLayerView != null && mLayerView.isExpanded()) { mLayerView.showExpandedView(selectedBubble); } } @@ -2108,6 +2147,13 @@ public class BubbleController implements ConfigurationChangeListener, } }; + private void showExpandedViewForBubbleBar() { + BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); + if (selectedBubble != null && mLayerView != null) { + mLayerView.showExpandedView(selectedBubble); + } + } + private void updateOverflowButtonDot() { BubbleOverflow overflow = mBubbleData.getOverflow(); if (overflow == null) return; @@ -2174,7 +2220,6 @@ public class BubbleController implements ConfigurationChangeListener, // And since all children are removed, remove the summary. removeCallback.accept(-1); - // TODO: (b/145659174) remove references to mSuppressedGroupKeys once fully migrated mBubbleData.addSummaryToSuppress(summary.getStatusBarNotification().getGroupKey(), summary.getKey()); } @@ -2314,6 +2359,7 @@ public class BubbleController implements ConfigurationChangeListener, * @param entry the entry to bubble. */ static boolean canLaunchInTaskView(Context context, BubbleEntry entry) { + if (Flags.enableBubbleAnything()) return true; PendingIntent intent = entry.getBubbleMetadata() != null ? entry.getBubbleMetadata().getIntent() : null; @@ -2430,6 +2476,16 @@ public class BubbleController implements ConfigurationChangeListener, } @Override + public void showShortcutBubble(ShortcutInfo info) { + mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(info)); + } + + @Override + public void showAppBubble(Intent intent) { + mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(intent)); + } + + @Override public void showBubble(String key, int topOnScreen) { mMainExecutor.execute( () -> mController.expandStackAndSelectBubbleFromLauncher(key, topOnScreen)); @@ -2456,8 +2512,8 @@ public class BubbleController implements ConfigurationChangeListener, } @Override - public void dragBubbleToDismiss(String key) { - mMainExecutor.execute(() -> mController.dragBubbleToDismiss(key)); + public void dragBubbleToDismiss(String key, long timestamp) { + mMainExecutor.execute(() -> mController.dragBubbleToDismiss(key, timestamp)); } @Override @@ -2479,6 +2535,15 @@ public class BubbleController implements ConfigurationChangeListener, if (mLayerView != null) mLayerView.updateExpandedView(); }); } + + @Override + public void showExpandedView() { + mMainExecutor.execute(() -> { + if (mLayerView != null) { + showExpandedViewForBubbleBar(); + } + }); + } } private class BubblesImpl implements Bubbles { @@ -2625,6 +2690,13 @@ public class BubbleController implements ConfigurationChangeListener, } @Override + public void expandStackAndSelectBubble(ShortcutInfo info) { + mMainExecutor.execute(() -> { + BubbleController.this.expandStackAndSelectBubble(info); + }); + } + + @Override public void expandStackAndSelectBubble(Bubble bubble) { mMainExecutor.execute(() -> { BubbleController.this.expandStackAndSelectBubble(bubble); 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 761e02598460..709a7bdc61f2 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 @@ -23,8 +23,10 @@ import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; import android.annotation.NonNull; import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; import android.content.LocusId; import android.content.pm.ShortcutInfo; +import android.os.UserHandle; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -35,12 +37,14 @@ import android.view.View; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.internal.util.FrameworkStatsLog; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubbles.DismissReason; -import com.android.wm.shell.common.bubbles.BubbleBarUpdate; -import com.android.wm.shell.common.bubbles.RemovedBubble; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; +import com.android.wm.shell.shared.bubbles.RemovedBubble; import java.io.PrintWriter; import java.util.ArrayList; @@ -150,8 +154,11 @@ public class BubbleData { : null; for (int i = 0; i < removedBubbles.size(); i++) { Pair<Bubble, Integer> pair = removedBubbles.get(i); - bubbleBarUpdate.removedBubbles.add( - new RemovedBubble(pair.first.getKey(), pair.second)); + // if the removal happened in launcher, don't send it back + if (pair.second != Bubbles.DISMISS_USER_GESTURE_FROM_LAUNCHER) { + bubbleBarUpdate.removedBubbles.add( + new RemovedBubble(pair.first.getKey(), pair.second)); + } } if (orderChanged) { // Include the new order @@ -171,6 +178,7 @@ public class BubbleData { BubbleBarUpdate getInitialState() { BubbleBarUpdate bubbleBarUpdate = BubbleBarUpdate.createInitialState(); bubbleBarUpdate.shouldShowEducation = shouldShowEducation; + bubbleBarUpdate.showOverflow = !overflowBubbles.isEmpty(); for (int i = 0; i < bubbles.size(); i++) { bubbleBarUpdate.currentBubbleList.add(bubbles.get(i).asBubbleBarBubble()); } @@ -195,6 +203,7 @@ public class BubbleData { private final BubblePositioner mPositioner; private final BubbleEducationController mEducationController; private final Executor mMainExecutor; + private final Executor mBgExecutor; /** Bubbles that are actively in the stack. */ private final List<Bubble> mBubbles; /** Bubbles that aged out to overflow. */ @@ -240,12 +249,14 @@ public class BubbleData { private HashMap<String, String> mSuppressedGroupKeys = new HashMap<>(); public BubbleData(Context context, BubbleLogger bubbleLogger, BubblePositioner positioner, - BubbleEducationController educationController, Executor mainExecutor) { + BubbleEducationController educationController, @ShellMainThread Executor mainExecutor, + @ShellBackgroundThread Executor bgExecutor) { mContext = context; mLogger = bubbleLogger; mPositioner = positioner; mEducationController = educationController; mMainExecutor = mainExecutor; + mBgExecutor = bgExecutor; mOverflow = new BubbleOverflow(context, positioner); mBubbles = new ArrayList<>(); mOverflowBubbles = new ArrayList<>(); @@ -417,23 +428,20 @@ public class BubbleData { Bubble bubbleToReturn = getBubbleInStackWithKey(key); if (bubbleToReturn == null) { - bubbleToReturn = getOverflowBubbleWithKey(key); - if (bubbleToReturn != null) { - // Promoting from overflow - mOverflowBubbles.remove(bubbleToReturn); - if (mOverflowBubbles.isEmpty()) { - mStateChange.showOverflowChanged = true; + // Check if it's in the overflow + bubbleToReturn = findAndRemoveBubbleFromOverflow(key); + if (bubbleToReturn == null) { + if (entry != null) { + // Not in the overflow, have an entry, so it's a new bubble + bubbleToReturn = new Bubble(entry, + mBubbleMetadataFlagListener, + mCancelledListener, + mMainExecutor, + mBgExecutor); + } else { + // If there's no entry it must be a persisted bubble + bubbleToReturn = persistedBubble; } - } else if (mPendingBubbles.containsKey(key)) { - // Update while it was pending - bubbleToReturn = mPendingBubbles.get(key); - } else if (entry != null) { - // New bubble - bubbleToReturn = new Bubble(entry, mBubbleMetadataFlagListener, mCancelledListener, - mMainExecutor); - } else { - // Persisted bubble being promoted - bubbleToReturn = persistedBubble; } } @@ -444,6 +452,46 @@ public class BubbleData { return bubbleToReturn; } + Bubble getOrCreateBubble(ShortcutInfo info) { + String bubbleKey = Bubble.getBubbleKeyForShortcut(info); + Bubble bubbleToReturn = findAndRemoveBubbleFromOverflow(bubbleKey); + if (bubbleToReturn == null) { + bubbleToReturn = Bubble.createShortcutBubble(info, mMainExecutor, mBgExecutor); + } + return bubbleToReturn; + } + + Bubble getOrCreateBubble(Intent intent) { + UserHandle user = UserHandle.of(mCurrentUserId); + String bubbleKey = Bubble.getAppBubbleKeyForApp(intent.getPackage(), + user); + Bubble bubbleToReturn = findAndRemoveBubbleFromOverflow(bubbleKey); + if (bubbleToReturn == null) { + bubbleToReturn = Bubble.createAppBubble(intent, user, null, mMainExecutor, mBgExecutor); + } + return bubbleToReturn; + } + + @Nullable + private Bubble findAndRemoveBubbleFromOverflow(String key) { + Bubble bubbleToReturn = getBubbleInStackWithKey(key); + if (bubbleToReturn != null) { + return bubbleToReturn; + } + bubbleToReturn = getOverflowBubbleWithKey(key); + if (bubbleToReturn != null) { + mOverflowBubbles.remove(bubbleToReturn); + // Promoting from overflow + mOverflowBubbles.remove(bubbleToReturn); + if (mOverflowBubbles.isEmpty()) { + mStateChange.showOverflowChanged = true; + } + } else if (mPendingBubbles.containsKey(key)) { + bubbleToReturn = mPendingBubbles.get(key); + } + return bubbleToReturn; + } + /** * When this method is called it is expected that all info in the bubble has completed loading. * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, BubbleExpandedViewManager, @@ -502,12 +550,34 @@ public class BubbleData { dispatchPendingChanges(); } + /** Dismisses the bubble with the matching key, if it exists. */ + public void dismissBubbleWithKey(String key, @DismissReason int reason) { + dismissBubbleWithKey(key, reason, mTimeSource.currentTimeMillis()); + } + /** * Dismisses the bubble with the matching key, if it exists. + * + * <p>This is used when the bubble was dismissed in launcher, where the {@code removalTimestamp} + * represents when the removal happened and can be used to check whether or not the bubble has + * been updated after the removal. If no updates, it's safe to remove the bubble, otherwise the + * removal is ignored. */ - public void dismissBubbleWithKey(String key, @DismissReason int reason) { - doRemove(key, reason); - dispatchPendingChanges(); + public void dismissBubbleWithKey(String key, @DismissReason int reason, long removalTimestamp) { + boolean shouldRemove = true; + // if the bubble was removed from launcher, verify that the removal happened after the last + // time it was updated + if (reason == Bubbles.DISMISS_USER_GESTURE_FROM_LAUNCHER) { + // if the bubble was removed from launcher it must be active. + Bubble bubble = getBubbleInStackWithKey(key); + if (bubble != null && bubble.getLastActivity() > removalTimestamp) { + shouldRemove = false; + } + } + if (shouldRemove) { + doRemove(key, reason); + dispatchPendingChanges(); + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt index df12999afc9d..818ba45bec42 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt @@ -31,6 +31,9 @@ import com.android.wm.shell.bubbles.storage.BubbleEntity import com.android.wm.shell.bubbles.storage.BubblePersistentRepository import com.android.wm.shell.bubbles.storage.BubbleVolatileRepository import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.annotations.ShellBackgroundThread +import com.android.wm.shell.shared.annotations.ShellMainThread +import java.util.concurrent.Executor import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -41,7 +44,8 @@ import kotlinx.coroutines.yield class BubbleDataRepository( private val launcherApps: LauncherApps, - private val mainExecutor: ShellExecutor, + @ShellMainThread private val mainExecutor: ShellExecutor, + @ShellBackgroundThread private val bgExecutor: Executor, private val persistentRepository: BubblePersistentRepository, ) { private val volatileRepository = BubbleVolatileRepository(launcherApps) @@ -259,8 +263,8 @@ class BubbleDataRepository( entity.locus, entity.isDismissable, mainExecutor, - bubbleMetadataFlagListener - ) + bgExecutor, + bubbleMetadataFlagListener) } } mainExecutor.execute { cb(bubbles) } 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 c7ccd50af550..52955267a501 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 @@ -16,7 +16,7 @@ package com.android.wm.shell.bubbles; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; @@ -67,11 +67,11 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.ScreenDecorationsUtils; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.common.AlphaOptimizedButton; -import com.android.wm.shell.common.TriangleShape; +import com.android.wm.shell.shared.TriangleShape; import com.android.wm.shell.taskview.TaskView; import java.io.PrintWriter; @@ -225,14 +225,16 @@ public class BubbleExpandedView extends LinearLayout { options.setTaskAlwaysOnTop(true); options.setLaunchedFromBubble(true); options.setPendingIntentBackgroundActivityStartMode( - MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); Intent fillInIntent = new Intent(); // Apply flags to make behaviour match documentLaunchMode=always. fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId() + || (mBubble.getShortcutInfo() != null && Flags.enableBubbleAnything())); + if (mBubble.isAppBubble()) { Context context = mContext.createContextAsUser( @@ -247,7 +249,8 @@ public class BubbleExpandedView extends LinearLayout { /* options= */ null); mTaskView.startActivity(pi, /* fillInIntent= */ null, options, launchBounds); - } else if (!mIsOverflow && mBubble.hasMetadataShortcutId()) { + } else if (!mIsOverflow && isShortcutBubble) { + ProtoLog.v(WM_SHELL_BUBBLES, "startingShortcutBubble=%s", getBubbleKey()); options.setApplyActivityFlagsForBubbles(true); mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), options, launchBounds); @@ -373,6 +376,7 @@ public class BubbleExpandedView extends LinearLayout { // ==> activity view // ==> manage button bringChildToFront(mManageButton); + setManageClickListener(); applyThemeAttrs(); @@ -503,6 +507,7 @@ public class BubbleExpandedView extends LinearLayout { R.layout.bubble_manage_button, this /* parent */, false /* attach */); addView(mManageButton); mManageButton.setVisibility(visibility); + setManageClickListener(); post(() -> { int touchAreaHeight = getResources().getDimensionPixelSize( @@ -647,9 +652,8 @@ public class BubbleExpandedView extends LinearLayout { } } - // TODO: Could listener be passed when we pass StackView / can we avoid setting this like this - void setManageClickListener(OnClickListener manageClickListener) { - mManageButton.setOnClickListener(manageClickListener); + private void setManageClickListener() { + mManageButton.setOnClickListener(v -> mStackView.onManageBubbleClicked()); } /** @@ -917,7 +921,11 @@ public class BubbleExpandedView extends LinearLayout { return; } boolean isNew = mBubble == null || didBackingContentChange(bubble); - if (isNew || bubble.getKey().equals(mBubble.getKey())) { + boolean isUpdate = bubble != null && mBubble != null + && bubble.getKey().equals(mBubble.getKey()); + ProtoLog.d(WM_SHELL_BUBBLES, "BubbleExpandedView - update bubble=%s; isNew=%b; isUpdate=%b", + bubble.getKey(), isNew, isUpdate); + if (isNew || isUpdate) { mBubble = bubble; mManageButton.setContentDescription(getResources().getString( R.string.bubbles_settings_button_description, bubble.getAppName())); @@ -1148,5 +1156,7 @@ public class BubbleExpandedView extends LinearLayout { pw.print(prefix); pw.println("BubbleExpandedView:"); pw.print(prefix); pw.print(" taskId: "); pw.println(mTaskId); pw.print(prefix); pw.print(" stackView: "); pw.println(mStackView); + pw.print(prefix); pw.print(" contentVisibility: "); pw.println(mIsContentVisible); + pw.print(prefix); pw.print(" isAnimating: "); pw.println(mIsAnimating); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt index 3d9bf032c1b0..ec4854b47aff 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt @@ -16,6 +16,8 @@ package com.android.wm.shell.bubbles +import com.android.wm.shell.shared.bubbles.BubbleBarLocation + /** Manager interface for bubble expanded views. */ interface BubbleExpandedViewManager { @@ -30,6 +32,7 @@ interface BubbleExpandedViewManager { fun isStackExpanded(): Boolean fun isShowingAsBubbleBar(): Boolean fun hideCurrentInputMethod() + fun updateBubbleBarLocation(location: BubbleBarLocation) companion object { /** @@ -78,6 +81,10 @@ interface BubbleExpandedViewManager { override fun hideCurrentInputMethod() { controller.hideCurrentInputMethod() } + + override fun updateBubbleBarLocation(location: BubbleBarLocation) { + controller.bubbleBarLocation = location + } } } } 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 42de401d9db9..1711dca4a8a3 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 @@ -19,8 +19,8 @@ package com.android.wm.shell.bubbles; import static android.graphics.Paint.ANTI_ALIAS_FLAG; import static android.graphics.Paint.FILTER_BITMAP_FLAG; -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.shared.animation.Interpolators.ALPHA_IN; +import static com.android.wm.shell.shared.animation.Interpolators.ALPHA_OUT; import android.animation.ArgbEvaluator; import android.content.Context; @@ -50,7 +50,7 @@ import android.widget.TextView; import androidx.annotation.Nullable; import com.android.wm.shell.R; -import com.android.wm.shell.common.TriangleShape; +import com.android.wm.shell.shared.TriangleShape; /** * Flyout view that appears as a 'chat bubble' alongside the bubble stack. The flyout can visually diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt index f32974e1765d..68c4657f2b68 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt @@ -80,7 +80,10 @@ class BubbleOverflow(private val context: Context, private val positioner: Bubbl expandedViewManager, positioner, /* isOverflow= */ true, - /* bubbleTaskView= */ null + /* bubbleTaskView= */ null, + /* mainExecutor= */ null, + /* backgroundExecutor= */ null, + /* regionSamplingProvider= */ null ) } 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 18e04d14c71b..bf98ef82b475 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 @@ -42,7 +42,7 @@ import androidx.annotation.Nullable; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.internal.util.ContrastColorUtil; import com.android.wm.shell.Flags; import com.android.wm.shell.R; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePopupViewExt.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePopupViewExt.kt index bdb09e11d5ad..fd110a276826 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePopupViewExt.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePopupViewExt.kt @@ -17,8 +17,8 @@ package com.android.wm.shell.bubbles import android.graphics.Color import com.android.wm.shell.R -import com.android.wm.shell.common.bubbles.BubblePopupDrawable -import com.android.wm.shell.common.bubbles.BubblePopupView +import com.android.wm.shell.shared.bubbles.BubblePopupDrawable +import com.android.wm.shell.shared.bubbles.BubblePopupView /** * A convenience method to setup the [BubblePopupView] with the correct config using local resources 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 2382545ab324..c386c9398624 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 @@ -29,10 +29,10 @@ import android.view.WindowManager; import androidx.annotation.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.R; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; /** * Keeps track of display size, configuration, and specific bubble sizes. One place for all 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 09bec8c37b9a..2795881f0938 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,15 +19,15 @@ 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.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING; import static com.android.wm.shell.bubbles.BubblePositioner.StackPinnedEdge.LEFT; import static com.android.wm.shell.bubbles.BubblePositioner.StackPinnedEdge.RIGHT; -import static com.android.wm.shell.common.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; +import static com.android.wm.shell.shared.animation.Interpolators.ALPHA_IN; +import static com.android.wm.shell.shared.animation.Interpolators.ALPHA_OUT; +import static com.android.wm.shell.shared.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -78,11 +78,10 @@ import androidx.dynamicanimation.animation.SpringForce; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.ScreenDecorationsUtils; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.internal.util.FrameworkStatsLog; import com.android.wm.shell.Flags; import com.android.wm.shell.R; -import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.bubbles.BubblesNavBarMotionEventHandler.MotionEventListener; import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix; import com.android.wm.shell.bubbles.animation.ExpandedAnimationController; @@ -92,10 +91,11 @@ import com.android.wm.shell.bubbles.animation.PhysicsAnimationLayout; import com.android.wm.shell.bubbles.animation.StackAnimationController; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.bubbles.DismissView; -import com.android.wm.shell.common.bubbles.RelativeTouchListener; -import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import com.android.wm.shell.shared.bubbles.DismissView; +import com.android.wm.shell.shared.bubbles.RelativeTouchListener; +import com.android.wm.shell.shared.animation.Interpolators; import com.android.wm.shell.shared.animation.PhysicsAnimator; +import com.android.wm.shell.shared.magnetictarget.MagnetizedObject; import java.io.PrintWriter; import java.math.BigDecimal; @@ -339,6 +339,7 @@ public class BubbleStackView extends FrameLayout pw.println(mExpandedViewContainer.getAnimationMatrix()); pw.print(" stack visibility : "); pw.println(getVisibility()); pw.print(" temporarilyInvisible: "); pw.println(mTemporarilyInvisible); + pw.print(" expandedViewTemporarilyHidden: "); pw.println(mExpandedViewTemporarilyHidden); mStackAnimationController.dump(pw); mExpandedAnimationController.dump(pw); @@ -1127,6 +1128,8 @@ public class BubbleStackView extends FrameLayout if (expandedView != null) { // We need to be Z ordered on top in order for alpha animations to work. expandedView.setSurfaceZOrderedOnTop(true); + ProtoLog.d(WM_SHELL_BUBBLES, "expandedViewAlphaAnimation - start=%s", + expandedView.getBubbleKey()); expandedView.setAnimating(true); mExpandedViewContainer.setVisibility(VISIBLE); } @@ -1142,6 +1145,8 @@ public class BubbleStackView extends FrameLayout // = 0f remains in effect. && !mExpandedViewTemporarilyHidden) { expandedView.setSurfaceZOrderedOnTop(false); + ProtoLog.d(WM_SHELL_BUBBLES, "expandedViewAlphaAnimation - end=%s", + expandedView.getBubbleKey()); expandedView.setAnimating(false); } } @@ -1374,7 +1379,6 @@ public class BubbleStackView extends FrameLayout // The menu itself should respect locale direction so the icons are on the correct side. mManageMenu.setLayoutDirection(LAYOUT_DIRECTION_LOCALE); addView(mManageMenu); - updateManageButtonListener(); } /** @@ -1602,6 +1606,11 @@ public class BubbleStackView extends FrameLayout getResources().getColor(android.R.color.system_neutral1_1000))); mManageMenuScrim.setBackgroundDrawable(new ColorDrawable( getResources().getColor(android.R.color.system_neutral1_1000))); + if (mShowingManage) { + // the manage menu location depends on the manage button location which may need a + // layout pass, so post this to the looper + post(() -> showManageMenu(true)); + } } /** @@ -2005,6 +2014,7 @@ public class BubbleStackView extends FrameLayout // and then remove our views (removing the icon view triggers the removal of the // bubble window so do that at the end of the animation so we see the scrim animate). BadgedImageView iconView = bubble.getIconView(); + final BubbleViewProvider expandedBubbleBeforeScrim = mExpandedBubble; showScrim(false, () -> { mRemovingLastBubbleWhileExpanded = false; bubble.cleanupExpandedView(); @@ -2013,7 +2023,17 @@ public class BubbleStackView extends FrameLayout } bubble.cleanupViews(); // cleans up the icon view updateExpandedView(); // resets state for no expanded bubble - mExpandedBubble = null; + // Bubble keys may not have changed if we receive an update to the same bubble. + // Compare bubble object instances to see if the expanded bubble has changed. + if (expandedBubbleBeforeScrim == mExpandedBubble) { + // Only clear expanded bubble if it has not changed since the scrim animation + // started. + // Scrim animation can take some time run and it is possible for a new bubble + // to be added while the animation is running. This causes the expanded + // bubble to change. Make sure we only clear the expanded bubble if it did + // not change between when the scrim animation started and completed. + mExpandedBubble = null; + } }); logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); return; @@ -2155,7 +2175,14 @@ public class BubbleStackView extends FrameLayout final BubbleViewProvider previouslySelected = mExpandedBubble; mExpandedBubble = bubbleToSelect; mExpandedViewAnimationController.setExpandedView(getExpandedView()); - + final String previouslySelectedKey = previouslySelected != null + ? previouslySelected.getKey() + : "null"; + final String newlySelectedKey = mExpandedBubble != null + ? mExpandedBubble.getKey() + : "null"; + ProtoLog.d(WM_SHELL_BUBBLES, "showNewlySelectedBubble b=%s, previouslySelected=%s," + + " mIsExpanded=%b", newlySelectedKey, previouslySelectedKey, mIsExpanded); if (mIsExpanded) { hideCurrentInputMethod(); @@ -2557,6 +2584,8 @@ public class BubbleStackView extends FrameLayout expandedView.setContentAlpha(0f); expandedView.setBackgroundAlpha(0f); + ProtoLog.d(WM_SHELL_BUBBLES, "animateBubbleExpansion, setAnimating true for bubble=%s", + expandedView.getBubbleKey()); // We'll be starting the alpha animation after a slight delay, so set this flag early // here. expandedView.setAnimating(true); @@ -2721,6 +2750,11 @@ public class BubbleStackView extends FrameLayout mAnimatingOutSurfaceAlphaAnimator.reverse(); mExpandedViewAlphaAnimator.start(); + if (mExpandedBubble != null) { + ProtoLog.d(WM_SHELL_BUBBLES, "animateSwitchBubbles, switchingTo b=%s", + mExpandedBubble.getKey()); + } + if (mPositioner.showBubblesVertically()) { float translationX = mStackAnimationController.isStackOnLeftSide() ? mAnimatingOutSurfaceContainer.getTranslationX() + mBubbleSize * 2 @@ -3375,14 +3409,6 @@ public class BubbleStackView extends FrameLayout mExpandedViewContainer.setAlpha(0f); mExpandedViewContainer.addView(bev); - postDelayed(() -> { - // Set the Manage button click handler from postDelayed. This appears to resolve - // a race condition with adding the BubbleExpandedView view to the expanded view - // container. Due to the race condition the click handler sometimes is not set up - // correctly and is never called. - updateManageButtonListener(); - }, 0); - if (!mIsExpansionAnimating) { mIsBubbleSwitchAnimating = true; mSurfaceSynchronizer.syncSurfaceAndRun(() -> { @@ -3392,13 +3418,8 @@ public class BubbleStackView extends FrameLayout } } - private void updateManageButtonListener() { - BubbleExpandedView bev = getExpandedView(); - if (mIsExpanded && bev != null) { - bev.setManageClickListener((view) -> { - showManageMenu(true /* show */); - }); - } + void onManageBubbleClicked() { + showManageMenu(true /* show */); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index 0b66bcb6930e..0c0fd7b10f6e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -15,7 +15,7 @@ */ package com.android.wm.shell.bubbles; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; @@ -35,7 +35,8 @@ import android.view.ViewGroup; import androidx.annotation.Nullable; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; +import com.android.wm.shell.Flags; import com.android.wm.shell.taskview.TaskView; /** @@ -59,6 +60,9 @@ public class BubbleTaskViewHelper { /** Called when back is pressed on the task root. */ void onBackPressed(); + + /** Called when task removal has started. */ + void onTaskRemovalStarted(); } private final Context mContext; @@ -103,14 +107,15 @@ public class BubbleTaskViewHelper { options.setTaskAlwaysOnTop(true); options.setLaunchedFromBubble(true); options.setPendingIntentBackgroundActivityStartMode( - MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); Intent fillInIntent = new Intent(); // Apply flags to make behaviour match documentLaunchMode=always. fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId() + || (mBubble.getShortcutInfo() != null && Flags.enableBubbleAnything())); if (mBubble.isAppBubble()) { Context context = mContext.createContextAsUser( @@ -125,7 +130,7 @@ public class BubbleTaskViewHelper { /* options= */ null); mTaskView.startActivity(pi, /* fillInIntent= */ null, options, launchBounds); - } else if (mBubble.hasMetadataShortcutId()) { + } else if (isShortcutBubble) { options.setApplyActivityFlagsForBubbles(true); mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), options, launchBounds); @@ -188,6 +193,7 @@ public class BubbleTaskViewHelper { ((ViewGroup) mParentView).removeView(mTaskView); mTaskView = null; } + mListener.onTaskRemovalStarted(); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java index 69119cf4338e..3982a237dd3b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java @@ -20,6 +20,7 @@ import static com.android.wm.shell.bubbles.BadgedImageView.DEFAULT_PATH_SIZE; import static com.android.wm.shell.bubbles.BadgedImageView.WHITE_SCRIM_ALPHA; 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.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; import android.annotation.NonNull; import android.annotation.Nullable; @@ -34,30 +35,32 @@ import android.graphics.Matrix; import android.graphics.Path; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; -import android.os.AsyncTask; import android.util.Log; import android.util.PathParser; import android.view.LayoutInflater; +import android.view.View; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; +import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.icons.BubbleIconFactory; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; +import com.android.wm.shell.shared.handles.RegionSamplingHelper; import java.lang.ref.WeakReference; import java.util.Objects; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; /** * Simple task to inflate views & load necessary info to display a bubble. */ -public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask.BubbleViewInfo> { +public class BubbleViewInfoTask { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleViewInfoTask" : TAG_BUBBLES; - /** * Callback to find out when the bubble has been inflated & necessary data loaded. */ @@ -68,17 +71,22 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask void onBubbleViewsReady(Bubble bubble); } - private Bubble mBubble; - private WeakReference<Context> mContext; - private WeakReference<BubbleExpandedViewManager> mExpandedViewManager; - private WeakReference<BubbleTaskViewFactory> mTaskViewFactory; - private WeakReference<BubblePositioner> mPositioner; - private WeakReference<BubbleStackView> mStackView; - private WeakReference<BubbleBarLayerView> mLayerView; - private BubbleIconFactory mIconFactory; - private boolean mSkipInflation; - private Callback mCallback; - private Executor mMainExecutor; + private final Bubble mBubble; + private final WeakReference<Context> mContext; + private final WeakReference<BubbleExpandedViewManager> mExpandedViewManager; + private final WeakReference<BubbleTaskViewFactory> mTaskViewFactory; + private final WeakReference<BubblePositioner> mPositioner; + private final WeakReference<BubbleStackView> mStackView; + private final WeakReference<BubbleBarLayerView> mLayerView; + private final BubbleIconFactory mIconFactory; + private final boolean mSkipInflation; + private final Callback mCallback; + private final Executor mMainExecutor; + private final Executor mBgExecutor; + + private final AtomicBoolean mStarted = new AtomicBoolean(); + private final AtomicBoolean mCancelled = new AtomicBoolean(); + private final AtomicBoolean mFinished = new AtomicBoolean(); /** * Creates a task to load information for the provided {@link Bubble}. Once all info @@ -94,7 +102,8 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask BubbleIconFactory factory, boolean skipInflation, Callback c, - Executor mainExecutor) { + Executor mainExecutor, + Executor bgExecutor) { mBubble = b; mContext = new WeakReference<>(context); mExpandedViewManager = new WeakReference<>(expandedViewManager); @@ -106,40 +115,132 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask mSkipInflation = skipInflation; mCallback = c; mMainExecutor = mainExecutor; + mBgExecutor = bgExecutor; } - @Override - protected BubbleViewInfo doInBackground(Void... voids) { + /** + * Load bubble view info in background using {@code bgExecutor} specified in constructor. + * <br> + * Use {@link #cancel()} to stop the task. + * + * @throws IllegalStateException if the task is already started + */ + public void start() { + verifyCanStart(); + if (mCancelled.get()) { + // We got cancelled even before start was called. Exit early + mFinished.set(true); + return; + } + mBgExecutor.execute(() -> { + if (mCancelled.get()) { + // We got cancelled while background executor was busy and this was waiting + mFinished.set(true); + return; + } + BubbleViewInfo viewInfo = loadViewInfo(); + if (mCancelled.get()) { + // Do not schedule anything on main executor if we got cancelled. + // Loading view info involves inflating views and it is possible we get cancelled + // during it. + mFinished.set(true); + return; + } + mMainExecutor.execute(() -> { + // Before updating view info check that we did not get cancelled while waiting + // main executor to pick up the work + if (!mCancelled.get()) { + updateViewInfo(viewInfo); + } + mFinished.set(true); + }); + }); + } + + private void verifyCanStart() { + if (mStarted.getAndSet(true)) { + throw new IllegalStateException("Task already started"); + } + } + + /** + * Load bubble view info synchronously. + * + * @throws IllegalStateException if the task is already started + */ + public void startSync() { + verifyCanStart(); + if (mCancelled.get()) { + mFinished.set(true); + return; + } + updateViewInfo(loadViewInfo()); + mFinished.set(true); + } + + /** + * Cancel the task. Stops the task from running if called before {@link #start()} or + * {@link #startSync()} + */ + public void cancel() { + mCancelled.set(true); + } + + /** + * Return {@code true} when the task has completed loading the view info. + */ + public boolean isFinished() { + return mFinished.get(); + } + + @Nullable + private BubbleViewInfo loadViewInfo() { if (!verifyState()) { // If we're in an inconsistent state, then switched modes and should just bail now. return null; } + ProtoLog.v(WM_SHELL_BUBBLES, "Task loading bubble view info key=%s", mBubble.getKey()); if (mLayerView.get() != null) { - return BubbleViewInfo.populateForBubbleBar(mContext.get(), mExpandedViewManager.get(), - mTaskViewFactory.get(), mPositioner.get(), mLayerView.get(), mIconFactory, - mBubble, mSkipInflation); + return BubbleViewInfo.populateForBubbleBar(mContext.get(), mTaskViewFactory.get(), + mLayerView.get(), mIconFactory, mBubble, mSkipInflation); } else { - return BubbleViewInfo.populate(mContext.get(), mExpandedViewManager.get(), - mTaskViewFactory.get(), mPositioner.get(), mStackView.get(), mIconFactory, - mBubble, mSkipInflation); + return BubbleViewInfo.populate(mContext.get(), mTaskViewFactory.get(), + mPositioner.get(), mStackView.get(), mIconFactory, mBubble, mSkipInflation); } } - @Override - protected void onPostExecute(BubbleViewInfo viewInfo) { - if (isCancelled() || viewInfo == null) { + private void updateViewInfo(@Nullable BubbleViewInfo viewInfo) { + if (viewInfo == null || !verifyState()) { return; } - - mMainExecutor.execute(() -> { - if (!verifyState()) { - return; + ProtoLog.v(WM_SHELL_BUBBLES, "Task updating bubble view info key=%s", mBubble.getKey()); + if (!mBubble.isInflated()) { + if (viewInfo.expandedView != null) { + ProtoLog.v(WM_SHELL_BUBBLES, "Task initializing expanded view key=%s", + mBubble.getKey()); + viewInfo.expandedView.initialize(mExpandedViewManager.get(), mStackView.get(), + mPositioner.get(), false /* isOverflow */, viewInfo.taskView); + } else if (viewInfo.bubbleBarExpandedView != null) { + ProtoLog.v(WM_SHELL_BUBBLES, "Task initializing bubble bar expanded view key=%s", + mBubble.getKey()); + viewInfo.bubbleBarExpandedView.initialize(mExpandedViewManager.get(), + mPositioner.get(), false /* isOverflow */, viewInfo.taskView, + mMainExecutor, mBgExecutor, new RegionSamplingProvider() { + @Override + public RegionSamplingHelper createHelper(View sampledView, + RegionSamplingHelper.SamplingCallback callback, + Executor backgroundExecutor, Executor mainExecutor) { + return RegionSamplingProvider.super.createHelper(sampledView, + callback, backgroundExecutor, mainExecutor); + } + }); } - mBubble.setViewInfo(viewInfo); - if (mCallback != null) { - mCallback.onBubbleViewsReady(mBubble); - } - }); + } + + mBubble.setViewInfo(viewInfo); + if (mCallback != null) { + mCallback.onBubbleViewsReady(mBubble); + } } private boolean verifyState() { @@ -157,6 +258,9 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask public static class BubbleViewInfo { // TODO(b/273312602): for foldables it might make sense to populate all of the views + // Only set if views where inflated as part of the task + @Nullable BubbleTaskView taskView; + // Always populated ShortcutInfo shortcutInfo; String appName; @@ -176,9 +280,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask @Nullable public static BubbleViewInfo populateForBubbleBar(Context c, - BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory taskViewFactory, - BubblePositioner positioner, BubbleBarLayerView layerView, BubbleIconFactory iconFactory, Bubble b, @@ -186,12 +288,11 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask BubbleViewInfo info = new BubbleViewInfo(); if (!skipInflation && !b.isInflated()) { - BubbleTaskView bubbleTaskView = b.getOrCreateBubbleTaskView(taskViewFactory); + ProtoLog.v(WM_SHELL_BUBBLES, "Task inflating bubble bar views key=%s", b.getKey()); + info.taskView = b.getOrCreateBubbleTaskView(taskViewFactory); LayoutInflater inflater = LayoutInflater.from(c); info.bubbleBarExpandedView = (BubbleBarExpandedView) inflater.inflate( R.layout.bubble_bar_expanded_view, layerView, false /* attachToRoot */); - info.bubbleBarExpandedView.initialize( - expandedViewManager, positioner, false /* isOverflow */, bubbleTaskView); } if (!populateCommonInfo(info, c, b, iconFactory)) { @@ -205,7 +306,6 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask @VisibleForTesting @Nullable public static BubbleViewInfo populate(Context c, - BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory taskViewFactory, BubblePositioner positioner, BubbleStackView stackView, @@ -216,17 +316,15 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask // View inflation: only should do this once per bubble if (!skipInflation && !b.isInflated()) { + ProtoLog.v(WM_SHELL_BUBBLES, "Task inflating bubble views key=%s", b.getKey()); LayoutInflater inflater = LayoutInflater.from(c); info.imageView = (BadgedImageView) inflater.inflate( R.layout.bubble_view, stackView, false /* attachToRoot */); info.imageView.initialize(positioner); - BubbleTaskView bubbleTaskView = b.getOrCreateBubbleTaskView(taskViewFactory); + info.taskView = b.getOrCreateBubbleTaskView(taskViewFactory); info.expandedView = (BubbleExpandedView) inflater.inflate( R.layout.bubble_expanded_view, stackView, false /* attachToRoot */); - info.expandedView.initialize( - expandedViewManager, stackView, positioner, false /* isOverflow */, - bubbleTaskView); } if (!populateCommonInfo(info, c, b, iconFactory)) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskLegacy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskLegacy.java new file mode 100644 index 000000000000..1b7bb0db6516 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskLegacy.java @@ -0,0 +1,362 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles; + +import static com.android.wm.shell.bubbles.BadgedImageView.DEFAULT_PATH_SIZE; +import static com.android.wm.shell.bubbles.BadgedImageView.WHITE_SCRIM_ALPHA; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; +import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ShortcutInfo; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Path; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.AsyncTask; +import android.util.Log; +import android.util.PathParser; +import android.view.LayoutInflater; +import android.view.View; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.graphics.ColorUtils; +import com.android.launcher3.icons.BitmapInfo; +import com.android.launcher3.icons.BubbleIconFactory; +import com.android.wm.shell.R; +import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; +import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; +import com.android.wm.shell.shared.handles.RegionSamplingHelper; + +import java.lang.ref.WeakReference; +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * Simple task to inflate views & load necessary info to display a bubble. + * + * @deprecated Deprecated since this is using an AsyncTask. Use {@link BubbleViewInfoTask} instead. + */ +@Deprecated +// TODO(b/353894869): remove once flag for loading view info with executors rolls out +public class BubbleViewInfoTaskLegacy extends + AsyncTask<Void, Void, BubbleViewInfoTaskLegacy.BubbleViewInfo> { + private static final String TAG = + TAG_WITH_CLASS_NAME ? "BubbleViewInfoTaskLegacy" : TAG_BUBBLES; + + + /** + * Callback to find out when the bubble has been inflated & necessary data loaded. + */ + public interface Callback { + /** + * Called when data has been loaded for the bubble. + */ + void onBubbleViewsReady(Bubble bubble); + } + + private Bubble mBubble; + private WeakReference<Context> mContext; + private WeakReference<BubbleExpandedViewManager> mExpandedViewManager; + private WeakReference<BubbleTaskViewFactory> mTaskViewFactory; + private WeakReference<BubblePositioner> mPositioner; + private WeakReference<BubbleStackView> mStackView; + private WeakReference<BubbleBarLayerView> mLayerView; + private BubbleIconFactory mIconFactory; + private boolean mSkipInflation; + private Callback mCallback; + private Executor mMainExecutor; + private Executor mBackgroundExecutor; + + /** + * Creates a task to load information for the provided {@link Bubble}. Once all info + * is loaded, {@link Callback} is notified. + */ + BubbleViewInfoTaskLegacy(Bubble b, + Context context, + BubbleExpandedViewManager expandedViewManager, + BubbleTaskViewFactory taskViewFactory, + BubblePositioner positioner, + @Nullable BubbleStackView stackView, + @Nullable BubbleBarLayerView layerView, + BubbleIconFactory factory, + boolean skipInflation, + Callback c, + Executor mainExecutor, + Executor backgroundExecutor) { + mBubble = b; + mContext = new WeakReference<>(context); + mExpandedViewManager = new WeakReference<>(expandedViewManager); + mTaskViewFactory = new WeakReference<>(taskViewFactory); + mPositioner = new WeakReference<>(positioner); + mStackView = new WeakReference<>(stackView); + mLayerView = new WeakReference<>(layerView); + mIconFactory = factory; + mSkipInflation = skipInflation; + mCallback = c; + mMainExecutor = mainExecutor; + mBackgroundExecutor = backgroundExecutor; + } + + @Override + protected BubbleViewInfo doInBackground(Void... voids) { + if (!verifyState()) { + // If we're in an inconsistent state, then switched modes and should just bail now. + return null; + } + if (mLayerView.get() != null) { + return BubbleViewInfo.populateForBubbleBar(mContext.get(), mExpandedViewManager.get(), + mTaskViewFactory.get(), mPositioner.get(), mLayerView.get(), mIconFactory, + mBubble, mSkipInflation, mMainExecutor, mBackgroundExecutor); + } else { + return BubbleViewInfo.populate(mContext.get(), mExpandedViewManager.get(), + mTaskViewFactory.get(), mPositioner.get(), mStackView.get(), mIconFactory, + mBubble, mSkipInflation); + } + } + + @Override + protected void onPostExecute(BubbleViewInfo viewInfo) { + if (isCancelled() || viewInfo == null) { + return; + } + + mMainExecutor.execute(() -> { + if (!verifyState()) { + return; + } + mBubble.setViewInfoLegacy(viewInfo); + if (mCallback != null) { + mCallback.onBubbleViewsReady(mBubble); + } + }); + } + + private boolean verifyState() { + if (mExpandedViewManager.get().isShowingAsBubbleBar()) { + return mLayerView.get() != null; + } else { + return mStackView.get() != null; + } + } + + /** + * Info necessary to render a bubble. + */ + @VisibleForTesting + public static class BubbleViewInfo { + // TODO(b/273312602): for foldables it might make sense to populate all of the views + + // Always populated + ShortcutInfo shortcutInfo; + String appName; + Bitmap rawBadgeBitmap; + + // Only populated when showing in taskbar + @Nullable BubbleBarExpandedView bubbleBarExpandedView; + + // These are only populated when not showing in taskbar + @Nullable BadgedImageView imageView; + @Nullable BubbleExpandedView expandedView; + int dotColor; + Path dotPath; + @Nullable Bubble.FlyoutMessage flyoutMessage; + Bitmap bubbleBitmap; + Bitmap badgeBitmap; + + @Nullable + public static BubbleViewInfo populateForBubbleBar(Context c, + BubbleExpandedViewManager expandedViewManager, + BubbleTaskViewFactory taskViewFactory, + BubblePositioner positioner, + BubbleBarLayerView layerView, + BubbleIconFactory iconFactory, + Bubble b, + boolean skipInflation, + Executor mainExecutor, + Executor backgroundExecutor) { + BubbleViewInfo info = new BubbleViewInfo(); + + if (!skipInflation && !b.isInflated()) { + BubbleTaskView bubbleTaskView = b.getOrCreateBubbleTaskView(taskViewFactory); + LayoutInflater inflater = LayoutInflater.from(c); + info.bubbleBarExpandedView = (BubbleBarExpandedView) inflater.inflate( + R.layout.bubble_bar_expanded_view, layerView, false /* attachToRoot */); + info.bubbleBarExpandedView.initialize( + expandedViewManager, positioner, false /* isOverflow */, bubbleTaskView, + mainExecutor, backgroundExecutor, new RegionSamplingProvider() { + @Override + public RegionSamplingHelper createHelper(View sampledView, + RegionSamplingHelper.SamplingCallback callback, + Executor backgroundExecutor, Executor mainExecutor) { + return RegionSamplingProvider.super.createHelper(sampledView, + callback, backgroundExecutor, mainExecutor); + } + }); + } + + if (!populateCommonInfo(info, c, b, iconFactory)) { + // if we failed to update common fields return null + return null; + } + + return info; + } + + @VisibleForTesting + @Nullable + public static BubbleViewInfo populate(Context c, + BubbleExpandedViewManager expandedViewManager, + BubbleTaskViewFactory taskViewFactory, + BubblePositioner positioner, + BubbleStackView stackView, + BubbleIconFactory iconFactory, + Bubble b, + boolean skipInflation) { + BubbleViewInfo info = new BubbleViewInfo(); + + // View inflation: only should do this once per bubble + if (!skipInflation && !b.isInflated()) { + LayoutInflater inflater = LayoutInflater.from(c); + info.imageView = (BadgedImageView) inflater.inflate( + R.layout.bubble_view, stackView, false /* attachToRoot */); + info.imageView.initialize(positioner); + + BubbleTaskView bubbleTaskView = b.getOrCreateBubbleTaskView(taskViewFactory); + info.expandedView = (BubbleExpandedView) inflater.inflate( + R.layout.bubble_expanded_view, stackView, false /* attachToRoot */); + info.expandedView.initialize( + expandedViewManager, stackView, positioner, false /* isOverflow */, + bubbleTaskView); + } + + if (!populateCommonInfo(info, c, b, iconFactory)) { + // if we failed to update common fields return null + return null; + } + + // Flyout + info.flyoutMessage = b.getFlyoutMessage(); + if (info.flyoutMessage != null) { + info.flyoutMessage.senderAvatar = + loadSenderAvatar(c, info.flyoutMessage.senderIcon); + } + return info; + } + } + + /** + * Modifies the given {@code info} object and populates common fields in it. + * + * <p>This method returns {@code true} if the update was successful and {@code false} otherwise. + * Callers should assume that the info object is unusable if the update was unsuccessful. + */ + private static boolean populateCommonInfo( + BubbleViewInfo info, Context c, Bubble b, BubbleIconFactory iconFactory) { + if (b.getShortcutInfo() != null) { + info.shortcutInfo = b.getShortcutInfo(); + } + + // App name & app icon + PackageManager pm = BubbleController.getPackageManagerForUser(c, + b.getUser().getIdentifier()); + ApplicationInfo appInfo; + Drawable badgedIcon; + Drawable appIcon; + try { + appInfo = pm.getApplicationInfo( + b.getPackageName(), + PackageManager.MATCH_UNINSTALLED_PACKAGES + | PackageManager.MATCH_DISABLED_COMPONENTS + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE + | PackageManager.MATCH_DIRECT_BOOT_AWARE); + if (appInfo != null) { + info.appName = String.valueOf(pm.getApplicationLabel(appInfo)); + } + appIcon = pm.getApplicationIcon(b.getPackageName()); + badgedIcon = pm.getUserBadgedIcon(appIcon, b.getUser()); + } catch (PackageManager.NameNotFoundException exception) { + // If we can't find package... don't think we should show the bubble. + Log.w(TAG, "Unable to find package: " + b.getPackageName()); + return false; + } + + Drawable bubbleDrawable = null; + try { + // Badged bubble image + bubbleDrawable = iconFactory.getBubbleDrawable(c, info.shortcutInfo, + b.getIcon()); + } catch (Exception e) { + // If we can't create the icon we'll default to the app icon + Log.w(TAG, "Exception creating icon for the bubble: " + b.getKey()); + } + + if (bubbleDrawable == null) { + // Default to app icon + bubbleDrawable = appIcon; + } + + BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon, + b.isImportantConversation()); + info.badgeBitmap = badgeBitmapInfo.icon; + // Raw badge bitmap never includes the important conversation ring + info.rawBadgeBitmap = b.isImportantConversation() + ? iconFactory.getBadgeBitmap(badgedIcon, false).icon + : badgeBitmapInfo.icon; + + float[] bubbleBitmapScale = new float[1]; + info.bubbleBitmap = iconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale); + + // Dot color & placement + Path iconPath = PathParser.createPathFromPathData( + c.getResources().getString(com.android.internal.R.string.config_icon_mask)); + Matrix matrix = new Matrix(); + float scale = bubbleBitmapScale[0]; + float radius = DEFAULT_PATH_SIZE / 2f; + matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, + radius /* pivot y */); + iconPath.transform(matrix); + info.dotPath = iconPath; + info.dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, + Color.WHITE, WHITE_SCRIM_ALPHA); + return true; + } + + @Nullable + static Drawable loadSenderAvatar(@NonNull final Context context, @Nullable final Icon icon) { + Objects.requireNonNull(context); + if (icon == null) return null; + try { + if (icon.getType() == Icon.TYPE_URI + || icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) { + context.grantUriPermission(context.getPackageName(), + icon.getUri(), Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + return icon.loadDrawable(context); + } catch (Exception e) { + Log.w(TAG, "loadSenderAvatar failed: " + e.getMessage()); + return null; + } + } +} 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 82af88d03b19..62895fe7c7cc 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 @@ -23,6 +23,7 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.app.NotificationChannel; import android.content.Intent; +import android.content.pm.ShortcutInfo; import android.content.pm.UserInfo; import android.graphics.drawable.Icon; import android.hardware.HardwareBuffer; @@ -37,9 +38,9 @@ import android.window.ScreenCapture.SynchronousScreenCaptureListener; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; -import com.android.wm.shell.common.bubbles.BubbleBarUpdate; import com.android.wm.shell.shared.annotations.ExternalThread; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; import java.lang.annotation.Retention; import java.lang.annotation.Target; @@ -62,7 +63,7 @@ public interface Bubbles { DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT, DISMISS_OVERFLOW_MAX_REACHED, DISMISS_SHORTCUT_REMOVED, DISMISS_PACKAGE_REMOVED, DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK, DISMISS_USER_ACCOUNT_REMOVED, - DISMISS_SWITCH_TO_STACK}) + DISMISS_SWITCH_TO_STACK, DISMISS_USER_GESTURE_FROM_LAUNCHER}) @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) @interface DismissReason { } @@ -84,6 +85,7 @@ public interface Bubbles { int DISMISS_RELOAD_FROM_DISK = 15; int DISMISS_USER_ACCOUNT_REMOVED = 16; int DISMISS_SWITCH_TO_STACK = 17; + int DISMISS_USER_GESTURE_FROM_LAUNCHER = 18; /** Returns a binder that can be passed to an external process to manipulate Bubbles. */ default IBubbles createExternalInterface() { @@ -117,6 +119,14 @@ public interface Bubbles { /** * Request the stack expand if needed, then select the specified Bubble as current. + * If no bubble exists for this entry, one is created. + * + * @param info the shortcut info to use to create the bubble. + */ + void expandStackAndSelectBubble(ShortcutInfo info); + + /** + * Request the stack expand if needed, then select the specified Bubble as current. * * @param bubble the bubble to be selected */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java index 137568458e3c..9429c9e71b3b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarGestureTracker.java @@ -29,7 +29,7 @@ import android.view.InputMonitor; import androidx.annotation.Nullable; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.bubbles.BubblesNavBarMotionEventHandler.MotionEventListener; /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java index b7107f09b17f..d4f53ab353ea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesNavBarMotionEventHandler.java @@ -28,7 +28,7 @@ import android.view.ViewConfiguration; import androidx.annotation.Nullable; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; /** * Handles {@link MotionEvent}s for bubbles that begin in the nav bar area diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissViewExt.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissViewExt.kt index 48692d41016e..00a81727a9ac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissViewExt.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissViewExt.kt @@ -18,7 +18,7 @@ package com.android.wm.shell.bubbles import com.android.wm.shell.R -import com.android.wm.shell.common.bubbles.DismissView +import com.android.wm.shell.shared.bubbles.DismissView fun DismissView.setup() { setup(DismissView.Config( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl index 1db556c04180..1855b938f48e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl @@ -18,8 +18,9 @@ package com.android.wm.shell.bubbles; import android.content.Intent; import android.graphics.Rect; +import android.content.pm.ShortcutInfo; import com.android.wm.shell.bubbles.IBubblesListener; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; /** * Interface that is exposed to remote callers (launcher) to manipulate the bubbles feature when @@ -33,7 +34,7 @@ interface IBubbles { oneway void showBubble(in String key, in int topOnScreen) = 3; - oneway void dragBubbleToDismiss(in String key) = 4; + oneway void dragBubbleToDismiss(in String key, in long timestamp) = 4; oneway void removeAllBubbles() = 5; @@ -48,4 +49,10 @@ interface IBubbles { oneway void updateBubbleBarTopOnScreen(in int topOnScreen) = 10; oneway void stopBubbleDrag(in BubbleBarLocation location, in int topOnScreen) = 11; + + oneway void showShortcutBubble(in ShortcutInfo info) = 12; + + oneway void showAppBubble(in Intent intent) = 13; + + oneway void showExpandedView() = 14; }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl index 14d29cd887bb..eb907dbb6597 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl @@ -17,7 +17,7 @@ package com.android.wm.shell.bubbles; import android.os.Bundle; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; /** * Listener interface that Launcher attaches to SystemUI to get bubbles callbacks. */ 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 da71b1c741bb..39a2a7b868a0 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 @@ -27,7 +27,7 @@ import android.widget.Button import android.widget.LinearLayout import com.android.internal.R.color.system_neutral1_900 import com.android.wm.shell.R -import com.android.wm.shell.animation.Interpolators +import com.android.wm.shell.shared.animation.Interpolators /** * User education view to highlight the manage button that allows a user to configure the settings diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RegionSamplingProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RegionSamplingProvider.java new file mode 100644 index 000000000000..30f5c8fd56c3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RegionSamplingProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 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.bubbles; + +import android.view.View; + +import com.android.wm.shell.shared.handles.RegionSamplingHelper; + +import java.util.concurrent.Executor; + +/** + * Wrapper to provide a {@link com.android.wm.shell.shared.handles.RegionSamplingHelper} to allow + * testing it. + */ +public interface RegionSamplingProvider { + + /** Creates and returns the region sampling helper */ + default RegionSamplingHelper createHelper(View sampledView, + RegionSamplingHelper.SamplingCallback callback, + Executor backgroundExecutor, + Executor mainExecutor) { + return new RegionSamplingHelper(sampledView, + callback, backgroundExecutor, mainExecutor); + } + +} 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 c4108c4129e9..16606198b240 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 @@ -26,7 +26,7 @@ import android.widget.LinearLayout import android.widget.TextView import com.android.internal.util.ContrastColorUtil import com.android.wm.shell.R -import com.android.wm.shell.animation.Interpolators +import com.android.wm.shell.shared.animation.Interpolators /** * User education view to highlight the collapsed stack of bubbles. Shown only the first time a user 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 f925eaef2c77..8f0dfb9e2215 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 @@ -33,13 +33,13 @@ import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.SpringForce; import com.android.wm.shell.R; -import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.bubbles.BadgedImageView; import com.android.wm.shell.bubbles.BubbleOverflow; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleStackView; -import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import com.android.wm.shell.shared.animation.Interpolators; import com.android.wm.shell.shared.animation.PhysicsAnimator; +import com.android.wm.shell.shared.magnetictarget.MagnetizedObject; import com.google.android.collect.Sets; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java index aa4129a14dbc..7cb537a24ce2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java @@ -38,11 +38,11 @@ import androidx.dynamicanimation.animation.FloatPropertyCompat; import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.animation.FlingAnimationUtils; -import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.bubbles.BubbleExpandedView; import com.android.wm.shell.bubbles.BubblePositioner; +import com.android.wm.shell.shared.animation.Interpolators; import java.util.ArrayList; import java.util.List; 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 47d4d07500d5..91585dc425eb 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 @@ -42,8 +42,8 @@ import com.android.wm.shell.bubbles.BadgedImageView; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleStackView; import com.android.wm.shell.common.FloatingContentCoordinator; -import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import com.android.wm.shell.shared.animation.PhysicsAnimator; +import com.android.wm.shell.shared.magnetictarget.MagnetizedObject; import com.google.android.collect.Sets; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java index 8e58db198b13..74c3748dccaf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java @@ -24,9 +24,9 @@ import static android.view.View.VISIBLE; import static android.view.View.X; import static android.view.View.Y; -import static com.android.wm.shell.animation.Interpolators.EMPHASIZED; -import static com.android.wm.shell.animation.Interpolators.EMPHASIZED_DECELERATE; import static com.android.wm.shell.bubbles.bar.BubbleBarExpandedView.CORNER_RADIUS; +import static com.android.wm.shell.shared.animation.Interpolators.EMPHASIZED; +import static com.android.wm.shell.shared.animation.Interpolators.EMPHASIZED_DECELERATE; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -42,13 +42,13 @@ import android.widget.FrameLayout; import androidx.annotation.Nullable; -import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.bubbles.BubbleOverflow; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleViewProvider; import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix; -import com.android.wm.shell.common.magnetictarget.MagnetizedObject.MagneticTarget; +import com.android.wm.shell.shared.animation.Interpolators; import com.android.wm.shell.shared.animation.PhysicsAnimator; +import com.android.wm.shell.shared.magnetictarget.MagnetizedObject.MagneticTarget; /** * Helper class to animate a {@link BubbleBarExpandedView} on a bubble. @@ -253,6 +253,7 @@ public class BubbleBarAnimationHelper { return; } setDragPivot(bbev); + bbev.setDragging(true); // Corner radius gets scaled, apply the reverse scale to ensure we have the desired radius final float cornerRadius = bbev.getDraggedCornerRadius() / EXPANDED_VIEW_DRAG_SCALE; @@ -329,6 +330,7 @@ public class BubbleBarAnimationHelper { public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); bbev.resetPivot(); + bbev.setDragging(false); } }); startNewDragAnimation(animatorSet); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index 972dce51e02b..ec235a5d84ab 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -19,21 +19,23 @@ package com.android.wm.shell.bubbles.bar; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import android.annotation.Nullable; -import android.app.ActivityManager; import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Color; import android.graphics.Insets; import android.graphics.Outline; import android.graphics.Rect; +import android.os.Bundle; import android.util.AttributeSet; import android.util.FloatProperty; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleExpandedViewManager; @@ -42,8 +44,12 @@ import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleTaskView; import com.android.wm.shell.bubbles.BubbleTaskViewHelper; import com.android.wm.shell.bubbles.Bubbles; +import com.android.wm.shell.bubbles.RegionSamplingProvider; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.handles.RegionSamplingHelper; import com.android.wm.shell.taskview.TaskView; +import java.util.concurrent.Executor; import java.util.function.Supplier; /** Expanded view of a bubble when it's part of the bubble bar. */ @@ -81,21 +87,41 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView private static final String TAG = BubbleBarExpandedView.class.getSimpleName(); private static final int INVALID_TASK_ID = -1; + private Bubble mBubble; private BubbleExpandedViewManager mManager; private BubblePositioner mPositioner; private boolean mIsOverflow; private BubbleTaskViewHelper mBubbleTaskViewHelper; private BubbleBarMenuViewController mMenuViewController; - private @Nullable Supplier<Rect> mLayerBoundsSupplier; - private @Nullable Listener mListener; + @Nullable + private Supplier<Rect> mLayerBoundsSupplier; + @Nullable + private Listener mListener; private BubbleBarHandleView mHandleView; - private @Nullable TaskView mTaskView; - private @Nullable BubbleOverflowContainerView mOverflowView; + @Nullable + private TaskView mTaskView; + @Nullable + private BubbleOverflowContainerView mOverflowView; + /** + * The handle shown in the caption area is tinted based on the background color of the area. + * This can vary so we sample the caption region and update the handle color based on that. + * If we're showing the overflow, the helper and executors will be null. + */ + @Nullable + private RegionSamplingHelper mRegionSamplingHelper; + @Nullable + private RegionSamplingProvider mRegionSamplingProvider; + @Nullable + private Executor mMainExecutor; + @Nullable + private Executor mBackgroundExecutor; + private final Rect mSampleRect = new Rect(); + private final int[] mLoc = new int[2]; + + /** Height of the caption inset at the top of the TaskView */ private int mCaptionHeight; - - private int mBackgroundColor; /** Corner radius used when view is resting */ private float mRestingCornerRadius = 0f; /** Corner radius applied while dragging */ @@ -110,6 +136,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView */ private boolean mIsContentVisible = false; private boolean mIsAnimating; + private boolean mIsDragging; public BubbleBarExpandedView(Context context) { this(context, null); @@ -148,27 +175,28 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView setOnTouchListener((v, event) -> true); } - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - // Hide manage menu when view disappears - mMenuViewController.hideMenu(false /* animated */); - } - /** Initializes the view, must be called before doing anything else. */ public void initialize(BubbleExpandedViewManager expandedViewManager, BubblePositioner positioner, boolean isOverflow, - @Nullable BubbleTaskView bubbleTaskView) { + @Nullable BubbleTaskView bubbleTaskView, + @Nullable Executor mainExecutor, + @Nullable Executor backgroundExecutor, + @Nullable RegionSamplingProvider regionSamplingProvider) { mManager = expandedViewManager; mPositioner = positioner; mIsOverflow = isOverflow; + mMainExecutor = mainExecutor; + mBackgroundExecutor = backgroundExecutor; + mRegionSamplingProvider = regionSamplingProvider; if (mIsOverflow) { mOverflowView = (BubbleOverflowContainerView) LayoutInflater.from(getContext()).inflate( R.layout.bubble_overflow_container, null /* root */); mOverflowView.initialize(expandedViewManager, positioner); addView(mOverflowView); + // Don't show handle for overflow + mHandleView.setVisibility(View.GONE); } else { mTaskView = bubbleTaskView.getTaskView(); mBubbleTaskViewHelper = new BubbleTaskViewHelper(mContext, expandedViewManager, @@ -183,15 +211,25 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView mTaskView.setEnableSurfaceClipping(true); mTaskView.setCornerRadius(mCurrentCornerRadius); mTaskView.setVisibility(VISIBLE); + mTaskView.setCaptionInsets(Insets.of(0, mCaptionHeight, 0, 0)); // Handle view needs to draw on top of task view. bringChildToFront(mHandleView); + + mHandleView.setAccessibilityDelegate(new HandleViewAccessibilityDelegate()); } mMenuViewController = new BubbleBarMenuViewController(mContext, this); mMenuViewController.setListener(new BubbleBarMenuViewController.Listener() { @Override public void onMenuVisibilityChanged(boolean visible) { setObscured(visible); + if (visible) { + mHandleView.setFocusable(false); + mHandleView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + } else { + mHandleView.setFocusable(true); + mHandleView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_AUTO); + } } @Override @@ -211,6 +249,13 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView public void onDismissBubble(Bubble bubble) { mManager.dismissBubble(bubble, Bubbles.DISMISS_USER_GESTURE); } + + @Override + public void onMoveToFullscreen(Bubble bubble) { + if (mTaskView != null) { + mTaskView.moveToFullscreen(); + } + } }); mHandleView.setOnClickListener(view -> { mMenuViewController.showMenu(true /* animated */); @@ -221,32 +266,40 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView return mHandleView; } - // TODO (b/275087636): call this when theme/config changes /** Updates the view based on the current theme. */ public void applyThemeAttrs() { + mCaptionHeight = getResources().getDimensionPixelSize( + R.dimen.bubble_bar_expanded_view_caption_height); mRestingCornerRadius = getResources().getDimensionPixelSize( - R.dimen.bubble_bar_expanded_view_corner_radius - ); + R.dimen.bubble_bar_expanded_view_corner_radius); mDraggedCornerRadius = getResources().getDimensionPixelSize( - R.dimen.bubble_bar_expanded_view_corner_radius_dragged - ); + R.dimen.bubble_bar_expanded_view_corner_radius_dragged); mCurrentCornerRadius = mRestingCornerRadius; - final TypedArray ta = mContext.obtainStyledAttributes(new int[]{ - android.R.attr.colorBackgroundFloating}); - mBackgroundColor = ta.getColor(0, Color.WHITE); - ta.recycle(); - mCaptionHeight = getResources().getDimensionPixelSize( - R.dimen.bubble_bar_expanded_view_caption_height); - if (mTaskView != null) { mTaskView.setCornerRadius(mCurrentCornerRadius); - updateHandleColor(true /* animated */); + mTaskView.setCaptionInsets(Insets.of(0, mCaptionHeight, 0, 0)); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + // Hide manage menu when view disappears + mMenuViewController.hideMenu(false /* animated */); + if (mRegionSamplingHelper != null) { + mRegionSamplingHelper.stopAndDestroy(); } } @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + recreateRegionSamplingHelper(); + } + + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mTaskView != null) { @@ -260,16 +313,13 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (mTaskView != null) { - mTaskView.layout(l, t, r, - t + mTaskView.getMeasuredHeight()); - mTaskView.setCaptionInsets(Insets.of(0, mCaptionHeight, 0, 0)); + mTaskView.layout(l, t, r, t + mTaskView.getMeasuredHeight()); } } @Override public void onTaskCreated() { setContentVisibility(true); - updateHandleColor(false /* animated */); if (mListener != null) { mListener.onTaskCreated(); } @@ -281,11 +331,70 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView } @Override + public void onTaskRemovalStarted() { + if (mRegionSamplingHelper != null) { + mRegionSamplingHelper.stopAndDestroy(); + } + } + + @Override public void onBackPressed() { if (mListener == null) return; mListener.onBackPressed(); } + /** + * Set whether this view is currently being dragged. + * + * When dragging, the handle is hidden and content shouldn't be sampled. When dragging has + * ended we should start again. + */ + public void setDragging(boolean isDragging) { + if (isDragging != mIsDragging) { + mIsDragging = isDragging; + updateSamplingState(); + } + } + + /** Returns whether region sampling should be enabled, i.e. if task view content is visible. */ + private boolean shouldSampleRegion() { + return mTaskView != null + && mTaskView.getTaskInfo() != null + && !mIsDragging + && !mIsAnimating + && mIsContentVisible; + } + + /** + * Handles starting or stopping the region sampling helper based on + * {@link #shouldSampleRegion()}. + */ + private void updateSamplingState() { + if (mRegionSamplingHelper == null) return; + boolean shouldSample = shouldSampleRegion(); + if (shouldSample) { + mRegionSamplingHelper.start(getCaptionSampleRect()); + } else { + mRegionSamplingHelper.stop(); + } + } + + /** Returns the current area of the caption bar, in screen coordinates. */ + Rect getCaptionSampleRect() { + if (mTaskView == null) return null; + mTaskView.getLocationOnScreen(mLoc); + mSampleRect.set(mLoc[0], mLoc[1], + mLoc[0] + mTaskView.getWidth(), + mLoc[1] + mCaptionHeight); + return mSampleRect; + } + + @VisibleForTesting + @Nullable + public RegionSamplingHelper getRegionSamplingHelper() { + return mRegionSamplingHelper; + } + /** Cleans up the expanded view, should be called when the bubble is no longer active. */ public void cleanUpExpandedState() { mMenuViewController.hideMenu(false /* animated */); @@ -317,6 +426,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView /** Updates the bubble shown in the expanded view. */ public void update(Bubble bubble) { + mBubble = bubble; mBubbleTaskViewHelper.update(bubble); mMenuViewController.updateMenu(bubble); } @@ -369,27 +479,14 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView if (!mIsAnimating) { mTaskView.setAlpha(visible ? 1f : 0f); + if (mRegionSamplingHelper != null) { + mRegionSamplingHelper.setWindowVisible(visible); + } + updateSamplingState(); } } /** - * Updates the handle color based on the task view status bar or background color; if those - * are transparent it defaults to the background color pulled from system theme attributes. - */ - private void updateHandleColor(boolean animated) { - if (mTaskView == null || mTaskView.getTaskInfo() == null) return; - int color = mBackgroundColor; - ActivityManager.TaskDescription taskDescription = mTaskView.getTaskInfo().taskDescription; - if (taskDescription.getStatusBarColor() != Color.TRANSPARENT) { - color = taskDescription.getStatusBarColor(); - } else if (taskDescription.getBackgroundColor() != Color.TRANSPARENT) { - color = taskDescription.getBackgroundColor(); - } - final boolean isRegionDark = Color.luminance(color) <= 0.5; - mHandleView.updateHandleColor(isRegionDark, animated); - } - - /** * Sets the alpha of both this view and the task view. */ public void setTaskViewAlpha(float alpha) { @@ -417,6 +514,11 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView */ public void setAnimating(boolean animating) { mIsAnimating = animating; + if (mIsAnimating) { + // Stop sampling while animating -- when animating is done setContentVisibility will + // re-trigger sampling if we're visible. + updateSamplingState(); + } // If we're done animating, apply the correct visibility. if (!animating) { setContentVisibility(mIsContentVisible); @@ -455,4 +557,82 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView invalidateOutline(); } } + + private void recreateRegionSamplingHelper() { + if (mRegionSamplingHelper != null) { + mRegionSamplingHelper.stopAndDestroy(); + } + if (mMainExecutor == null || mBackgroundExecutor == null + || mRegionSamplingProvider == null) { + // Null when it's the overflow / don't need sampling then. + return; + } + mRegionSamplingHelper = mRegionSamplingProvider.createHelper(this, + new RegionSamplingHelper.SamplingCallback() { + @Override + public void onRegionDarknessChanged(boolean isRegionDark) { + if (mHandleView != null) { + mHandleView.updateHandleColor(isRegionDark, + true /* animated */); + } + } + + @Override + public Rect getSampledRegion(View sampledView) { + return getCaptionSampleRect(); + } + + @Override + public boolean isSamplingEnabled() { + return shouldSampleRegion(); + } + }, mMainExecutor, mBackgroundExecutor); + } + + private class HandleViewAccessibilityDelegate extends AccessibilityDelegate { + @Override + public void onInitializeAccessibilityNodeInfo(@NonNull View host, + @NonNull AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.addAction(new AccessibilityNodeInfo.AccessibilityAction( + AccessibilityNodeInfo.ACTION_CLICK, getResources().getString( + R.string.bubble_accessibility_action_expand_menu))); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS); + if (mPositioner.isBubbleBarOnLeft()) { + info.addAction(new AccessibilityNodeInfo.AccessibilityAction( + R.id.action_move_bubble_bar_right, getResources().getString( + R.string.bubble_accessibility_action_move_bar_right))); + } else { + info.addAction(new AccessibilityNodeInfo.AccessibilityAction( + R.id.action_move_bubble_bar_left, getResources().getString( + R.string.bubble_accessibility_action_move_bar_left))); + } + } + + @Override + public boolean performAccessibilityAction(@NonNull View host, int action, + @Nullable Bundle args) { + if (super.performAccessibilityAction(host, action, args)) { + return true; + } + if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) { + mManager.collapseStack(); + return true; + } + if (action == AccessibilityNodeInfo.ACTION_DISMISS) { + mManager.dismissBubble(mBubble, Bubbles.DISMISS_USER_GESTURE); + return true; + } + if (action == R.id.action_move_bubble_bar_left) { + mManager.updateBubbleBarLocation(BubbleBarLocation.LEFT); + return true; + } + if (action == R.id.action_move_bubble_bar_right) { + mManager.updateBubbleBarLocation(BubbleBarLocation.RIGHT); + return true; + } + return false; + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt index fa1091c63d00..07463bb024a2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt @@ -20,9 +20,9 @@ import android.annotation.SuppressLint import android.view.MotionEvent import android.view.View import com.android.wm.shell.bubbles.BubblePositioner -import com.android.wm.shell.common.bubbles.DismissView -import com.android.wm.shell.common.bubbles.RelativeTouchListener -import com.android.wm.shell.common.magnetictarget.MagnetizedObject +import com.android.wm.shell.shared.bubbles.DismissView +import com.android.wm.shell.shared.bubbles.RelativeTouchListener +import com.android.wm.shell.shared.magnetictarget.MagnetizedObject /** Controller for handling drag interactions with [BubbleBarExpandedView] */ @SuppressLint("ClickableViewAccessibility") @@ -38,6 +38,9 @@ class BubbleBarExpandedViewDragController( var isStuckToDismiss: Boolean = false private set + var isDragged: Boolean = false + private set + private var expandedViewInitialTranslationX = 0f private var expandedViewInitialTranslationY = 0f private val magnetizedExpandedView: MagnetizedObject<BubbleBarExpandedView> = @@ -94,6 +97,7 @@ class BubbleBarExpandedViewDragController( // While animating, don't allow new touch events if (expandedView.isAnimating) return false pinController.onDragStart(bubblePositioner.isBubbleBarOnLeft) + isDragged = true return true } @@ -141,6 +145,7 @@ class BubbleBarExpandedViewDragController( dismissView.hide() } isMoving = false + isDragged = false } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java index d54a6b002e43..e781c07f01a7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java @@ -42,7 +42,9 @@ public class BubbleBarHandleView extends View { private final @ColorInt int mHandleLightColor; private final @ColorInt int mHandleDarkColor; - private @Nullable ObjectAnimator mColorChangeAnim; + private @ColorInt int mCurrentColor; + @Nullable + private ObjectAnimator mColorChangeAnim; public BubbleBarHandleView(Context context) { this(context, null /* attrs */); @@ -80,6 +82,7 @@ public class BubbleBarHandleView extends View { outline.setPath(mPath); } }); + setContentDescription(getResources().getString(R.string.handle_text)); } /** @@ -87,13 +90,17 @@ public class BubbleBarHandleView extends View { * * @param isRegionDark Whether the background behind the handle is dark, and thus the handle * should be light (and vice versa). - * @param animated Whether to animate the change, or apply it immediately. + * @param animated Whether to animate the change, or apply it immediately. */ public void updateHandleColor(boolean isRegionDark, boolean animated) { int newColor = isRegionDark ? mHandleLightColor : mHandleDarkColor; + if (newColor == mCurrentColor) { + return; + } if (mColorChangeAnim != null) { mColorChangeAnim.cancel(); } + mCurrentColor = newColor; if (animated) { mColorChangeAnim = ObjectAnimator.ofArgb(this, "backgroundColor", newColor); mColorChangeAnim.addListener(new AnimatorListenerAdapter() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index badc40997902..1367b7e24bc7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -16,8 +16,8 @@ package com.android.wm.shell.bubbles.bar; -import static com.android.wm.shell.animation.Interpolators.ALPHA_IN; -import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT; +import static com.android.wm.shell.shared.animation.Interpolators.ALPHA_IN; +import static com.android.wm.shell.shared.animation.Interpolators.ALPHA_OUT; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_GESTURE; import android.annotation.Nullable; @@ -44,9 +44,9 @@ import com.android.wm.shell.bubbles.BubbleViewProvider; import com.android.wm.shell.bubbles.DeviceConfig; import com.android.wm.shell.bubbles.DismissViewUtils; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedViewDragController.DragListener; -import com.android.wm.shell.common.bubbles.BaseBubblePinController; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; -import com.android.wm.shell.common.bubbles.DismissView; +import com.android.wm.shell.shared.bubbles.BaseBubblePinController; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.DismissView; import kotlin.Unit; @@ -175,12 +175,21 @@ public class BubbleBarLayerView extends FrameLayout return mIsExpanded; } + /** Return whether the expanded view is being dragged */ + public boolean isExpandedViewDragged() { + return mDragController != null && mDragController.isDragged(); + } + /** Shows the expanded view of the provided bubble. */ public void showExpandedView(BubbleViewProvider b) { BubbleBarExpandedView expandedView = b.getBubbleBarExpandedView(); if (expandedView == null) { return; } + if (mExpandedBubble != null && mIsExpanded && b.getKey().equals(mExpandedBubble.getKey())) { + // Already showing this bubble, skip animating + return; + } if (mExpandedBubble != null && !b.getKey().equals(mExpandedBubble.getKey())) { removeView(mExpandedView); mExpandedView = null; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuItemView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuItemView.java index 00b977721bea..1c71ef415eae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuItemView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuItemView.java @@ -64,7 +64,7 @@ public class BubbleBarMenuItemView extends LinearLayout { void update(Icon icon, String title, @ColorInt int tint) { if (tint == Color.TRANSPARENT) { final TypedArray typedArray = getContext().obtainStyledAttributes( - new int[]{android.R.attr.textColorPrimary}); + new int[]{com.android.internal.R.attr.materialColorOnSurface}); mTextView.setTextColor(typedArray.getColor(0, Color.BLACK)); } else { icon.setTint(tint); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java index 211fe0d48e43..0300869cbbe1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java @@ -17,15 +17,21 @@ package com.android.wm.shell.bubbles.bar; import android.annotation.ColorInt; import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.drawable.Icon; import android.util.AttributeSet; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.core.widget.ImageViewCompat; + import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubble; @@ -38,6 +44,7 @@ public class BubbleBarMenuView extends LinearLayout { private ViewGroup mBubbleSectionView; private ViewGroup mActionsSectionView; private ImageView mBubbleIconView; + private ImageView mBubbleDismissIconView; private TextView mBubbleTitleView; public BubbleBarMenuView(Context context) { @@ -64,6 +71,29 @@ public class BubbleBarMenuView extends LinearLayout { mActionsSectionView = findViewById(R.id.bubble_bar_manage_menu_actions_section); mBubbleIconView = findViewById(R.id.bubble_bar_manage_menu_bubble_icon); mBubbleTitleView = findViewById(R.id.bubble_bar_manage_menu_bubble_title); + mBubbleDismissIconView = findViewById(R.id.bubble_bar_manage_menu_dismiss_icon); + updateThemeColors(); + + mBubbleSectionView.setAccessibilityDelegate(new AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.addAction(new AccessibilityNodeInfo.AccessibilityAction( + AccessibilityNodeInfo.ACTION_CLICK, getResources().getString( + R.string.bubble_accessibility_action_collapse_menu))); + } + }); + } + + private void updateThemeColors() { + try (TypedArray ta = mContext.obtainStyledAttributes(new int[]{ + com.android.internal.R.attr.materialColorSurfaceBright, + com.android.internal.R.attr.materialColorOnSurface + })) { + mActionsSectionView.getBackground().setTint(ta.getColor(0, Color.WHITE)); + ImageViewCompat.setImageTintList(mBubbleDismissIconView, + ColorStateList.valueOf(ta.getColor(1, Color.BLACK))); + } } /** Update menu details with bubble info */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java index 02918db124e3..514810745e10 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java @@ -19,15 +19,17 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; import android.graphics.drawable.Icon; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import androidx.core.content.ContextCompat; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.SpringForce; +import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.shared.animation.PhysicsAnimator; @@ -172,12 +174,17 @@ class BubbleBarMenuViewController { private ArrayList<BubbleBarMenuView.MenuAction> createMenuActions(Bubble bubble) { ArrayList<BubbleBarMenuView.MenuAction> menuActions = new ArrayList<>(); Resources resources = mContext.getResources(); - + int tintColor; + try (TypedArray ta = mContext.obtainStyledAttributes(new int[]{ + com.android.internal.R.attr.materialColorOnSurface})) { + tintColor = ta.getColor(0, Color.TRANSPARENT); + } if (bubble.isConversation()) { // Don't bubble conversation action menuActions.add(new BubbleBarMenuView.MenuAction( Icon.createWithResource(mContext, R.drawable.bubble_ic_stop_bubble), resources.getString(R.string.bubbles_dont_bubble_conversation), + tintColor, view -> { hideMenu(true /* animated */); if (mListener != null) { @@ -204,7 +211,7 @@ class BubbleBarMenuViewController { menuActions.add(new BubbleBarMenuView.MenuAction( Icon.createWithResource(resources, R.drawable.ic_remove_no_shadow), resources.getString(R.string.bubble_dismiss_text), - ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_menu_close), + tintColor, view -> { hideMenu(true /* animated */); if (mListener != null) { @@ -213,6 +220,21 @@ class BubbleBarMenuViewController { } )); + if (Flags.enableBubbleAnything() || Flags.enableBubbleToFullscreen()) { + menuActions.add(new BubbleBarMenuView.MenuAction( + Icon.createWithResource(resources, + R.drawable.desktop_mode_ic_handle_menu_fullscreen), + resources.getString(R.string.bubble_fullscreen_text), + tintColor, + view -> { + hideMenu(true /* animated */); + if (mListener != null) { + mListener.onMoveToFullscreen(bubble); + } + } + )); + } + return menuActions; } @@ -243,5 +265,10 @@ class BubbleBarMenuViewController { * Dismiss bubble and remove it from the bubble stack */ void onDismissBubble(Bubble bubble); + + /** + * Move the bubble to fullscreen. + */ + void onMoveToFullscreen(Bubble bubble); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt index e108f7be48c7..9fd255ded0ad 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt @@ -34,9 +34,9 @@ import com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME import com.android.wm.shell.bubbles.BubbleEducationController import com.android.wm.shell.bubbles.BubbleViewProvider import com.android.wm.shell.bubbles.setup -import com.android.wm.shell.common.bubbles.BubblePopupDrawable -import com.android.wm.shell.common.bubbles.BubblePopupView import com.android.wm.shell.shared.animation.PhysicsAnimator +import com.android.wm.shell.shared.bubbles.BubblePopupDrawable +import com.android.wm.shell.shared.bubbles.BubblePopupView import kotlin.math.roundToInt /** Manages bubble education presentation and animation */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt index 651bf022e07d..23ba2bff5ebc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt @@ -25,8 +25,8 @@ import android.widget.FrameLayout import androidx.core.view.updateLayoutParams import com.android.wm.shell.R import com.android.wm.shell.bubbles.BubblePositioner -import com.android.wm.shell.common.bubbles.BaseBubblePinController -import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.android.wm.shell.shared.bubbles.BaseBubblePinController +import com.android.wm.shell.shared.bubbles.BubbleBarLocation /** * Controller to manage pinning bubble bar to left or right when dragging starts from the bubble bar diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/CreateBubbleShortcutActivity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/CreateBubbleShortcutActivity.kt index a124f95d7431..c93c11eb2fc2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/CreateBubbleShortcutActivity.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/CreateBubbleShortcutActivity.kt @@ -20,10 +20,10 @@ import android.app.Activity import android.content.pm.ShortcutManager import android.graphics.drawable.Icon import android.os.Bundle +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.Flags import com.android.wm.shell.R import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES -import com.android.wm.shell.util.KtProtoLog /** Activity to create a shortcut to open bubbles */ class CreateBubbleShortcutActivity : Activity() { @@ -31,7 +31,7 @@ class CreateBubbleShortcutActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (Flags.enableRetrievableBubbles()) { - KtProtoLog.d(WM_SHELL_BUBBLES, "Creating a shortcut for bubbles") + ProtoLog.d(WM_SHELL_BUBBLES, "Creating a shortcut for bubbles") createShortcut() } finish() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/ShowBubblesActivity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/ShowBubblesActivity.kt index ae7940ca1b65..e578e9e76979 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/ShowBubblesActivity.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/ShowBubblesActivity.kt @@ -21,9 +21,9 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.os.Bundle +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.Flags import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES -import com.android.wm.shell.util.KtProtoLog /** Activity that sends a broadcast to open bubbles */ class ShowBubblesActivity : Activity() { @@ -37,7 +37,7 @@ class ShowBubblesActivity : Activity() { // Set the package as the receiver is not exported `package` = packageName } - KtProtoLog.v(WM_SHELL_BUBBLES, "Sending broadcast to show bubbles") + ProtoLog.v(WM_SHELL_BUBBLES, "Sending broadcast to show bubbles") sendBroadcast(intent) } finish() 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 dcbc72ab0d32..f532be6b8277 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 @@ -23,6 +23,7 @@ import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.os.RemoteException; import android.util.ArraySet; +import android.util.Size; import android.util.Slog; import android.util.SparseArray; import android.view.Display; @@ -193,8 +194,8 @@ public class DisplayController { /** Called when a display rotate requested. */ - public void onDisplayRotateRequested(WindowContainerTransaction wct, int displayId, - int fromRotation, int toRotation) { + public void onDisplayChangeRequested(WindowContainerTransaction wct, int displayId, + Rect startAbsBounds, Rect endAbsBounds, int fromRotation, int toRotation) { synchronized (mDisplays) { final DisplayRecord dr = mDisplays.get(displayId); if (dr == null) { @@ -203,7 +204,16 @@ public class DisplayController { } if (dr.mDisplayLayout != null) { - dr.mDisplayLayout.rotateTo(dr.mContext.getResources(), toRotation); + if (endAbsBounds != null) { + // If there is a change in the display dimensions update the layout as well; + // note that endAbsBounds should ignore any potential rotation changes, so + // we still need to rotate the layout after if needed. + dr.mDisplayLayout.resizeTo(dr.mContext.getResources(), + new Size(endAbsBounds.width(), endAbsBounds.height())); + } + if (fromRotation != toRotation) { + dr.mDisplayLayout.rotateTo(dr.mContext.getResources(), toRotation); + } } mChangeController.dispatchOnDisplayChange( 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 f4ac5f260fcd..c4082d9f649c 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 @@ -52,6 +52,7 @@ import android.view.inputmethod.InputMethodManagerGlobal; import androidx.annotation.VisibleForTesting; import com.android.internal.inputmethod.SoftInputShowHideReason; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.sysui.ShellInit; import java.util.ArrayList; @@ -290,17 +291,11 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged if (hadImeSourceControl != hasImeSourceControl) { dispatchImeControlTargetChanged(mDisplayId, hasImeSourceControl); } + final boolean hasImeLeash = hasImeSourceControl && imeSourceControl.getLeash() != null; boolean pendingImeStartAnimation = false; - boolean canAnimate; - if (android.view.inputmethod.Flags.refactorInsetsController()) { - canAnimate = hasImeSourceControl && imeSourceControl.getLeash() != null; - } else { - canAnimate = hasImeSourceControl; - } - boolean positionChanged = false; - if (canAnimate) { + if (hasImeLeash) { if (mAnimation != null) { final Point lastSurfacePosition = hadImeSourceControl ? mImeSourceControl.getSurfacePosition() : null; @@ -308,22 +303,37 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged lastSurfacePosition); } else { if (!haveSameLeash(mImeSourceControl, imeSourceControl)) { - applyVisibilityToLeash(imeSourceControl); - if (android.view.inputmethod.Flags.refactorInsetsController()) { pendingImeStartAnimation = true; + // The starting point for the IME should be it's previous state + // (whether it is initiallyVisible or not) + updateImeVisibility(imeSourceControl.isInitiallyVisible()); } + applyVisibilityToLeash(imeSourceControl); } if (!mImeShowing) { - removeImeSurface(); + removeImeSurface(mDisplayId); } } - } else if (!android.view.inputmethod.Flags.refactorInsetsController() - && mAnimation != null) { - // we don"t want to cancel the hide animation, when the control is lost, but - // continue the bar to slide to the end (even without visible IME) - mAnimation.cancel(); + } else { + if (!android.view.inputmethod.Flags.refactorInsetsController() + && mAnimation != null) { + // we don't want to cancel the hide animation, when the control is lost, but + // continue the bar to slide to the end (even without visible IME) + mAnimation.cancel(); + } else if (android.view.inputmethod.Flags.refactorInsetsController() && mImeShowing + && mAnimation == null) { + // There is no leash, so the IME cannot be in a showing state + updateImeVisibility(false); + } } + + // Make mImeSourceControl point to the new control before starting the animation. + if (hadImeSourceControl && mImeSourceControl != imeSourceControl) { + mImeSourceControl.release(SurfaceControl::release); + } + mImeSourceControl = imeSourceControl; + if (positionChanged) { if (android.view.inputmethod.Flags.refactorInsetsController()) { // For showing the IME, the leash has to be available first. Hiding @@ -337,15 +347,9 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } } - if (hadImeSourceControl && mImeSourceControl != imeSourceControl) { - mImeSourceControl.release(SurfaceControl::release); - } - mImeSourceControl = imeSourceControl; - if (android.view.inputmethod.Flags.refactorInsetsController()) { if (pendingImeStartAnimation) { - startAnimation(true, true /* forceRestart */, - null /* statsToken */); + startAnimation(mImeRequestedVisible, true /* forceRestart */); } } } @@ -398,8 +402,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged // already (e.g., when focussing an editText in activity B, while and editText in // activity A is focussed), we will not get a call of #insetsControlChanged, and // therefore have to start the show animation from here - startAnimation(mImeRequestedVisible /* show */, false /* forceRestart */, - null /* TODO statsToken */); + startAnimation(mImeRequestedVisible /* show */, false /* forceRestart */); } } @@ -436,29 +439,51 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged .navBarFrameHeight(); } + private void startAnimation(final boolean show, final boolean forceRestart) { + final var imeSource = mInsetsState.peekSource(InsetsSource.ID_IME); + if (imeSource == null || mImeSourceControl == null) { + return; + } + final var statsToken = mImeSourceControl.getImeStatsToken(); + + startAnimation(show, forceRestart, statsToken); + } + private void startAnimation(final boolean show, final boolean forceRestart, @SoftInputShowHideReason int reason) { final var imeSource = mInsetsState.peekSource(InsetsSource.ID_IME); if (imeSource == null || mImeSourceControl == null) { return; } - final var statsToken = ImeTracker.forLogging().onStart( - show ? ImeTracker.TYPE_SHOW : ImeTracker.TYPE_HIDE, ImeTracker.ORIGIN_WM_SHELL, - reason, false /* fromUser */); - + final ImeTracker.Token statsToken; + if (android.view.inputmethod.Flags.refactorInsetsController() + && mImeSourceControl.getImeStatsToken() != null) { + statsToken = mImeSourceControl.getImeStatsToken(); + } else { + statsToken = ImeTracker.forLogging().onStart( + show ? ImeTracker.TYPE_SHOW : ImeTracker.TYPE_HIDE, + ImeTracker.ORIGIN_WM_SHELL, reason, false /* fromUser */); + } startAnimation(show, forceRestart, statsToken); } private void startAnimation(final boolean show, final boolean forceRestart, @NonNull final ImeTracker.Token statsToken) { + if (mImeSourceControl == null || mImeSourceControl.getLeash() == null) { + if (DEBUG) Slog.d(TAG, "No leash available, not starting the animation."); + return; + } if (android.view.inputmethod.Flags.refactorInsetsController()) { - if (mImeSourceControl == null || mImeSourceControl.getLeash() == null) { - if (DEBUG) Slog.d(TAG, "No leash available, not starting the animation."); + if (!mImeRequestedVisible && show) { + // we have a control with leash, but the IME was not requested visible before, + // therefore aborting the show animation. + Slog.e(TAG, "IME was not requested visible, not starting the show animation."); + // TODO(b/353463205) fail statsToken here return; } } final InsetsSource imeSource = mInsetsState.peekSource(InsetsSource.ID_IME); - if (imeSource == null || mImeSourceControl == null) { + if (imeSource == null) { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE); return; } @@ -495,8 +520,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } mAnimation.cancel(); } - final float defaultY = mImeSourceControl.getSurfacePosition().y; - final float x = mImeSourceControl.getSurfacePosition().x; + final InsetsSourceControl animatingControl = new InsetsSourceControl(mImeSourceControl); + final SurfaceControl animatingLeash = animatingControl.getLeash(); + final float defaultY = animatingControl.getSurfacePosition().y; + final float x = animatingControl.getSurfacePosition().x; final float hiddenY = defaultY + mImeFrame.height(); final float shownY = defaultY; final float startY = show ? hiddenY : shownY; @@ -518,13 +545,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged mAnimation.addUpdateListener(animation -> { SurfaceControl.Transaction t = mTransactionPool.acquire(); float value = (float) animation.getAnimatedValue(); - if (!android.view.inputmethod.Flags.refactorInsetsController() || ( - mImeSourceControl != null && mImeSourceControl.getLeash() != null)) { - t.setPosition(mImeSourceControl.getLeash(), x, value); - final float alpha = (mAnimateAlpha || isFloating) - ? (value - hiddenY) / (shownY - hiddenY) : 1.f; - t.setAlpha(mImeSourceControl.getLeash(), alpha); - } + t.setPosition(animatingLeash, x, value); + final float alpha = (mAnimateAlpha || isFloating) + ? (value - hiddenY) / (shownY - hiddenY) : 1f; + t.setAlpha(animatingLeash, alpha); dispatchPositionChanged(mDisplayId, imeTop(value), t); t.apply(); mTransactionPool.release(t); @@ -541,7 +565,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged ValueAnimator valueAnimator = (ValueAnimator) animation; float value = (float) valueAnimator.getAnimatedValue(); SurfaceControl.Transaction t = mTransactionPool.acquire(); - t.setPosition(mImeSourceControl.getLeash(), x, value); + t.setPosition(animatingLeash, x, value); if (DEBUG) { Slog.d(TAG, "onAnimationStart d:" + mDisplayId + " top:" + imeTop(hiddenY) + "->" + imeTop(shownY) @@ -553,19 +577,19 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged final float alpha = (mAnimateAlpha || isFloating) ? (value - hiddenY) / (shownY - hiddenY) : 1.f; - t.setAlpha(mImeSourceControl.getLeash(), alpha); + t.setAlpha(animatingLeash, alpha); if (mAnimationDirection == DIRECTION_SHOW) { ImeTracker.forLogging().onProgress(mStatsToken, ImeTracker.PHASE_WM_ANIMATION_RUNNING); - t.show(mImeSourceControl.getLeash()); + t.show(animatingLeash); } if (DEBUG_IME_VISIBILITY) { EventLog.writeEvent(IMF_IME_REMOTE_ANIM_START, mStatsToken != null ? mStatsToken.getTag() : ImeTracker.TOKEN_NONE, mDisplayId, mAnimationDirection, alpha, value, endY, - Objects.toString(mImeSourceControl.getLeash()), - Objects.toString(mImeSourceControl.getInsetsHint()), - Objects.toString(mImeSourceControl.getSurfacePosition()), + Objects.toString(animatingLeash), + Objects.toString(animatingControl.getInsetsHint()), + Objects.toString(animatingControl.getSurfacePosition()), Objects.toString(mImeFrame)); } t.apply(); @@ -579,32 +603,24 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged EventLog.writeEvent(IMF_IME_REMOTE_ANIM_CANCEL, mStatsToken != null ? mStatsToken.getTag() : ImeTracker.TOKEN_NONE, mDisplayId, - Objects.toString(mImeSourceControl.getInsetsHint())); + Objects.toString(animatingControl.getInsetsHint())); } } @Override public void onAnimationEnd(Animator animation) { - boolean hasLeash = - mImeSourceControl != null && mImeSourceControl.getLeash() != null; if (DEBUG) Slog.d(TAG, "onAnimationEnd " + mCancelled); SurfaceControl.Transaction t = mTransactionPool.acquire(); if (!mCancelled) { - if (!android.view.inputmethod.Flags.refactorInsetsController() - || hasLeash) { - t.setPosition(mImeSourceControl.getLeash(), x, endY); - t.setAlpha(mImeSourceControl.getLeash(), 1.f); - } + t.setPosition(animatingLeash, x, endY); + t.setAlpha(animatingLeash, 1.f); } dispatchEndPositioning(mDisplayId, mCancelled, t); if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) { ImeTracker.forLogging().onProgress(mStatsToken, ImeTracker.PHASE_WM_ANIMATION_RUNNING); - if (!android.view.inputmethod.Flags.refactorInsetsController() - || hasLeash) { - t.hide(mImeSourceControl.getLeash()); - } - removeImeSurface(); + t.hide(animatingLeash); + removeImeSurface(mDisplayId); ImeTracker.forLogging().onHidden(mStatsToken); } else if (mAnimationDirection == DIRECTION_SHOW && !mCancelled) { ImeTracker.forLogging().onShown(mStatsToken); @@ -616,13 +632,9 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged EventLog.writeEvent(IMF_IME_REMOTE_ANIM_END, mStatsToken != null ? mStatsToken.getTag() : ImeTracker.TOKEN_NONE, mDisplayId, mAnimationDirection, endY, - Objects.toString( - mImeSourceControl != null ? mImeSourceControl.getLeash() - : "null"), - Objects.toString(mImeSourceControl != null - ? mImeSourceControl.getInsetsHint() : "null"), - Objects.toString(mImeSourceControl != null - ? mImeSourceControl.getSurfacePosition() : "null"), + Objects.toString(animatingLeash), + Objects.toString(animatingControl.getInsetsHint()), + Objects.toString(animatingControl.getSurfacePosition()), Objects.toString(mImeFrame)); } t.apply(); @@ -630,6 +642,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged mAnimationDirection = DIRECTION_NONE; mAnimation = null; + animatingControl.release(SurfaceControl::release); } }); if (!show) { @@ -658,10 +671,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } } - void removeImeSurface() { + void removeImeSurface(int displayId) { // Remove the IME surface to make the insets invisible for // non-client controlled insets. - InputMethodManagerGlobal.removeImeSurface( + InputMethodManagerGlobal.removeImeSurface(displayId, e -> Slog.e(TAG, "Failed to remove IME surface.", e)); } 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 index 1fb0e1745e3e..c4c177cbcc28 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java @@ -47,6 +47,8 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan private final SparseArray<PerDisplay> mInsetsPerDisplay = new SparseArray<>(); private final SparseArray<CopyOnWriteArrayList<OnInsetsChangedListener>> mListeners = new SparseArray<>(); + private final CopyOnWriteArrayList<OnInsetsChangedListener> mGlobalListeners = + new CopyOnWriteArrayList<>(); public DisplayInsetsController(IWindowManager wmService, ShellInit shellInit, @@ -81,6 +83,16 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan } /** + * Adds a callback to listen for insets changes for any display. Note that the + * listener will not be updated with the existing state of the insets on any display. + */ + public void addGlobalInsetsChangedListener(OnInsetsChangedListener listener) { + if (!mGlobalListeners.contains(listener)) { + mGlobalListeners.add(listener); + } + } + + /** * Removes a callback listening for insets changes from a particular display. */ public void removeInsetsChangedListener(int displayId, OnInsetsChangedListener listener) { @@ -91,6 +103,13 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan listeners.remove(listener); } + /** + * Removes a callback listening for insets changes from any display. + */ + public void removeGlobalInsetsChangedListener(OnInsetsChangedListener listener) { + mGlobalListeners.remove(listener); + } + @Override public void onDisplayAdded(int displayId) { PerDisplay pd = new PerDisplay(displayId); @@ -138,12 +157,17 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan private void insetsChanged(InsetsState insetsState) { CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); - if (listeners == null) { + if (listeners == null && mGlobalListeners.isEmpty()) { return; } mDisplayController.updateDisplayInsets(mDisplayId, insetsState); - for (OnInsetsChangedListener listener : listeners) { - listener.insetsChanged(insetsState); + for (OnInsetsChangedListener listener : mGlobalListeners) { + listener.insetsChanged(mDisplayId, insetsState); + } + if (listeners != null) { + for (OnInsetsChangedListener listener : listeners) { + listener.insetsChanged(mDisplayId, insetsState); + } } } @@ -285,6 +309,13 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan default void insetsChanged(InsetsState insetsState) {} /** + * Called when the window insets configuration has changed for the given display. + */ + default void insetsChanged(int displayId, InsetsState insetsState) { + insetsChanged(insetsState); + } + + /** * Called when this window retrieved control over a specified set of insets sources. */ default void insetsControlChanged(InsetsState insetsState, 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 86cec02ab138..b6a1686bd087 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 @@ -35,6 +35,7 @@ import android.graphics.Rect; import android.os.SystemProperties; import android.provider.Settings; import android.util.DisplayMetrics; +import android.util.Size; import android.view.Display; import android.view.DisplayCutout; import android.view.DisplayInfo; @@ -81,6 +82,7 @@ public class DisplayLayout { private boolean mHasNavigationBar = false; private boolean mHasStatusBar = false; private int mNavBarFrameHeight = 0; + private int mTaskbarFrameHeight = 0; private boolean mAllowSeamlessRotationDespiteNavBarMoving = false; private boolean mNavigationBarCanMove = false; private boolean mReverseDefaultRotation = false; @@ -119,6 +121,7 @@ public class DisplayLayout { && mNavigationBarCanMove == other.mNavigationBarCanMove && mReverseDefaultRotation == other.mReverseDefaultRotation && mNavBarFrameHeight == other.mNavBarFrameHeight + && mTaskbarFrameHeight == other.mTaskbarFrameHeight && Objects.equals(mInsetsState, other.mInsetsState); } @@ -126,7 +129,7 @@ public class DisplayLayout { public int hashCode() { return Objects.hash(mUiMode, mWidth, mHeight, mCutout, mRotation, mDensityDpi, mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar, - mNavBarFrameHeight, mAllowSeamlessRotationDespiteNavBarMoving, + mNavBarFrameHeight, mTaskbarFrameHeight, mAllowSeamlessRotationDespiteNavBarMoving, mNavigationBarCanMove, mReverseDefaultRotation, mInsetsState); } @@ -176,6 +179,7 @@ public class DisplayLayout { mNavigationBarCanMove = dl.mNavigationBarCanMove; mReverseDefaultRotation = dl.mReverseDefaultRotation; mNavBarFrameHeight = dl.mNavBarFrameHeight; + mTaskbarFrameHeight = dl.mTaskbarFrameHeight; mNonDecorInsets.set(dl.mNonDecorInsets); mStableInsets.set(dl.mStableInsets); mInsetsState.set(dl.mInsetsState, true /* copySources */); @@ -214,7 +218,8 @@ public class DisplayLayout { if (mHasStatusBar) { convertNonDecorInsetsToStableInsets(res, mStableInsets, mCutout, mHasStatusBar); } - mNavBarFrameHeight = getNavigationBarFrameHeight(res, mWidth > mHeight); + mNavBarFrameHeight = getNavigationBarFrameHeight(res, /* landscape */ mWidth > mHeight); + mTaskbarFrameHeight = SystemBarUtils.getTaskbarHeight(res); } /** @@ -240,6 +245,16 @@ public class DisplayLayout { recalcInsets(res); } + /** + * Update the dimensions of this layout. + */ + public void resizeTo(Resources res, Size displaySize) { + mWidth = displaySize.getWidth(); + mHeight = displaySize.getHeight(); + + recalcInsets(res); + } + /** Get this layout's non-decor insets. */ public Rect nonDecorInsets() { return mNonDecorInsets; @@ -321,6 +336,17 @@ public class DisplayLayout { outBounds.inset(mStableInsets); } + /** Predicts the calculated stable bounds when in Desktop Mode. */ + public void getStableBoundsForDesktopMode(Rect outBounds) { + getStableBounds(outBounds); + + if (mNavBarFrameHeight != mTaskbarFrameHeight) { + // Currently not in pinned taskbar mode, exclude taskbar insets instead of current + // navigation insets from bounds. + outBounds.bottom = mHeight - mTaskbarFrameHeight; + } + } + /** * Gets navigation bar position for this layout * @return Navigation bar position for this layout. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java index bfee820870f1..736d954513b1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java @@ -54,4 +54,11 @@ public class HandlerExecutor implements ShellExecutor { public boolean hasCallback(Runnable r) { return mHandler.hasCallbacks(r); } + + @Override + public void assertCurrentThread() { + if (!mHandler.getLooper().isCurrentThread()) { + throw new IllegalStateException("must be called on " + mHandler); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ImeListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ImeListener.kt new file mode 100644 index 000000000000..a34d7bed497b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ImeListener.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 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.graphics.Rect +import android.view.InsetsSource +import android.view.InsetsState +import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener + +abstract class ImeListener( + private val mDisplayController: DisplayController, + private val mDisplayId: Int +) : OnInsetsChangedListener { + // The last insets state + private val mInsetsState = InsetsState() + private val mTmpBounds = Rect() + + override fun insetsChanged(insetsState: InsetsState) { + if (mInsetsState == insetsState) { + return + } + + // Get the stable bounds that account for display cutout and system bars to calculate the + // relative IME height + val layout = mDisplayController.getDisplayLayout(mDisplayId) + if (layout == null) { + return + } + layout.getStableBounds(mTmpBounds) + + val wasVisible = getImeVisibilityAndHeight(mInsetsState).first + val oldHeight = getImeVisibilityAndHeight(mInsetsState).second + + val isVisible = getImeVisibilityAndHeight(insetsState).first + val newHeight = getImeVisibilityAndHeight(insetsState).second + + mInsetsState.set(insetsState, true) + if (wasVisible != isVisible || oldHeight != newHeight) { + onImeVisibilityChanged(isVisible, newHeight) + } + } + + private fun getImeVisibilityAndHeight( + insetsState: InsetsState): Pair<Boolean, Int> { + val source = insetsState.peekSource(InsetsSource.ID_IME) + val frame = if (source != null && source.isVisible) source.frame else null + val height = if (frame != null) mTmpBounds.bottom - frame.top else 0 + val visible = source?.isVisible ?: false + return Pair(visible, height) + } + + /** + * To be overridden by implementations to handle IME changes. + */ + protected abstract fun onImeVisibilityChanged(imeVisible: Boolean, imeHeight: Int) +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/InteractionJankMonitorUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/InteractionJankMonitorUtils.java deleted file mode 100644 index 86f00b83cadd..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/InteractionJankMonitorUtils.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.common; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; -import android.text.TextUtils; -import android.view.SurfaceControl; -import android.view.View; - -import com.android.internal.jank.Cuj.CujType; -import com.android.internal.jank.InteractionJankMonitor; - -/** Utils class for simplfy InteractionJank trancing call */ -public class InteractionJankMonitorUtils { - - /** - * Begin a trace session. - * - * @param cujType the specific {@link CujType}. - * @param view the view to trace - * @param tag the tag to distinguish different flow of same type CUJ. - */ - public static void beginTracing(@CujType int cujType, - @NonNull View view, @Nullable String tag) { - final InteractionJankMonitor.Configuration.Builder builder = - InteractionJankMonitor.Configuration.Builder.withView(cujType, view); - if (!TextUtils.isEmpty(tag)) { - builder.setTag(tag); - } - InteractionJankMonitor.getInstance().begin(builder); - } - - /** - * Begin a trace session. - * - * @param cujType the specific {@link CujType}. - * @param context the context - * @param surface the surface to trace - * @param tag the tag to distinguish different flow of same type CUJ. - */ - public static void beginTracing(@CujType int cujType, - @NonNull Context context, @NonNull SurfaceControl surface, @Nullable String tag) { - final InteractionJankMonitor.Configuration.Builder builder = - InteractionJankMonitor.Configuration.Builder.withSurface(cujType, context, surface); - if (!TextUtils.isEmpty(tag)) { - builder.setTag(tag); - } - InteractionJankMonitor.getInstance().begin(builder); - } - - /** - * End a trace session. - * - * @param cujType the specific {@link CujType}. - */ - public static void endTracing(@CujType int cujType) { - InteractionJankMonitor.getInstance().end(cujType); - } - - /** - * Cancel the trace session. - * - * @param cujType the specific {@link CujType}. - */ - public static void cancelTracing(@CujType int cujType) { - InteractionJankMonitor.getInstance().cancel(cujType); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/LaunchAdjacentController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/LaunchAdjacentController.kt index 81592c35e4ac..e92b0b59d2ef 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/LaunchAdjacentController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/LaunchAdjacentController.kt @@ -17,8 +17,8 @@ package com.android.wm.shell.common import android.window.WindowContainerToken import android.window.WindowContainerTransaction +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG -import com.android.wm.shell.util.KtProtoLog /** * Controller to manage behavior of activities launched with @@ -30,7 +30,7 @@ class LaunchAdjacentController(private val syncQueue: SyncTransactionQueue) { var launchAdjacentEnabled: Boolean = true set(value) { if (field != value) { - KtProtoLog.d(WM_SHELL_TASK_ORG, "set launch adjacent flag root enabled=%b", value) + ProtoLog.d(WM_SHELL_TASK_ORG, "set launch adjacent flag root enabled=%b", value) field = value container?.let { c -> if (value) { @@ -52,7 +52,7 @@ class LaunchAdjacentController(private val syncQueue: SyncTransactionQueue) { * @see WindowContainerTransaction.setLaunchAdjacentFlagRoot */ fun setLaunchAdjacentRoot(container: WindowContainerToken) { - KtProtoLog.d(WM_SHELL_TASK_ORG, "set new launch adjacent flag root container") + ProtoLog.d(WM_SHELL_TASK_ORG, "set new launch adjacent flag root container") this.container = container if (launchAdjacentEnabled) { enableContainer(container) @@ -67,7 +67,7 @@ class LaunchAdjacentController(private val syncQueue: SyncTransactionQueue) { * @see WindowContainerTransaction.clearLaunchAdjacentFlagRoot */ fun clearLaunchAdjacentRoot() { - KtProtoLog.d(WM_SHELL_TASK_ORG, "clear launch adjacent flag root container") + ProtoLog.d(WM_SHELL_TASK_ORG, "clear launch adjacent flag root container") container?.let { disableContainer(it) container = null @@ -75,14 +75,14 @@ class LaunchAdjacentController(private val syncQueue: SyncTransactionQueue) { } private fun enableContainer(container: WindowContainerToken) { - KtProtoLog.v(WM_SHELL_TASK_ORG, "enable launch adjacent flag root container") + ProtoLog.v(WM_SHELL_TASK_ORG, "enable launch adjacent flag root container") val wct = WindowContainerTransaction() wct.setLaunchAdjacentFlagRoot(container) syncQueue.queue(wct) } private fun disableContainer(container: WindowContainerToken) { - KtProtoLog.v(WM_SHELL_TASK_ORG, "disable launch adjacent flag root container") + ProtoLog.v(WM_SHELL_TASK_ORG, "disable launch adjacent flag root container") val wct = WindowContainerTransaction() wct.clearLaunchAdjacentFlagRoot(container) syncQueue.queue(wct) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt index 9e8dfb5f0c6f..4cd2fd04d3cf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt @@ -22,10 +22,9 @@ import android.content.pm.LauncherApps import android.content.pm.PackageManager import android.os.UserHandle import android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI -import com.android.internal.annotations.VisibleForTesting +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.R import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL -import com.android.wm.shell.util.KtProtoLog import java.util.Arrays /** @@ -41,7 +40,6 @@ class MultiInstanceHelper @JvmOverloads constructor( /** * Returns whether a specific component desires to be launched in multiple instances. */ - @VisibleForTesting fun supportsMultiInstanceSplit(componentName: ComponentName?): Boolean { if (componentName == null || componentName.packageName == null) { // TODO(b/262864589): Handle empty component case @@ -52,7 +50,7 @@ class MultiInstanceHelper @JvmOverloads constructor( val packageName = componentName.packageName for (pkg in staticAppsSupportingMultiInstance) { if (pkg == packageName) { - KtProtoLog.v(WM_SHELL, "application=%s in allowlist supports multi-instance", + ProtoLog.v(WM_SHELL, "application=%s in allowlist supports multi-instance", packageName) return true } @@ -60,7 +58,7 @@ class MultiInstanceHelper @JvmOverloads constructor( if (!supportsMultiInstanceProperty) { // If not checking the multi-instance properties, then return early - return false; + return false } // Check the activity property first @@ -70,10 +68,10 @@ class MultiInstanceHelper @JvmOverloads constructor( // If the above call doesn't throw a NameNotFoundException, then the activity property // should override the application property value if (activityProp.isBoolean) { - KtProtoLog.v(WM_SHELL, "activity=%s supports multi-instance", componentName) + ProtoLog.v(WM_SHELL, "activity=%s supports multi-instance", componentName) return activityProp.boolean } else { - KtProtoLog.w(WM_SHELL, "Warning: property=%s for activity=%s has non-bool type=%d", + ProtoLog.w(WM_SHELL, "Warning: property=%s for activity=%s has non-bool type=%d", PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName, activityProp.type) } } catch (nnfe: PackageManager.NameNotFoundException) { @@ -85,10 +83,10 @@ class MultiInstanceHelper @JvmOverloads constructor( val appProp = packageManager.getProperty( PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName) if (appProp.isBoolean) { - KtProtoLog.v(WM_SHELL, "application=%s supports multi-instance", packageName) + ProtoLog.v(WM_SHELL, "application=%s supports multi-instance", packageName) return appProp.boolean } else { - KtProtoLog.w(WM_SHELL, + ProtoLog.w(WM_SHELL, "Warning: property=%s for application=%s has non-bool type=%d", PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName, appProp.type) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java index f729164ed303..2c2961fd4b65 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java @@ -96,4 +96,11 @@ public interface ShellExecutor extends Executor { * See {@link android.os.Handler#hasCallbacks(Runnable)}. */ boolean hasCallback(Runnable runnable); + + /** + * May throw if the caller is not on the same thread as the executor. + */ + default void assertCurrentThread() { + return; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java index 4b138e43bc3f..dd17e2980e58 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java @@ -17,7 +17,6 @@ package com.android.wm.shell.common; import android.view.SurfaceControl; -import android.view.SurfaceSession; /** * Helpers for handling surface. @@ -25,16 +24,15 @@ import android.view.SurfaceSession; public class SurfaceUtils { /** Creates a dim layer above host surface. */ public static SurfaceControl makeDimLayer(SurfaceControl.Transaction t, SurfaceControl host, - String name, SurfaceSession surfaceSession) { - final SurfaceControl dimLayer = makeColorLayer(host, name, surfaceSession); + String name) { + final SurfaceControl dimLayer = makeColorLayer(host, name); t.setLayer(dimLayer, Integer.MAX_VALUE).setColor(dimLayer, new float[]{0f, 0f, 0f}); return dimLayer; } /** Creates a color layer for host surface. */ - public static SurfaceControl makeColorLayer(SurfaceControl host, String name, - SurfaceSession surfaceSession) { - return new SurfaceControl.Builder(surfaceSession) + public static SurfaceControl makeColorLayer(SurfaceControl host, String name) { + return new SurfaceControl.Builder() .setParent(host) .setColorLayer() .setName(name) 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 e261d92bda5c..bcd40a9a9765 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 @@ -28,7 +28,8 @@ import android.window.WindowContainerTransaction; import android.window.WindowContainerTransactionCallback; import android.window.WindowOrganizer; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.transition.LegacyTransitions; import java.util.ArrayList; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java index ef33b3830e45..3dc86decdb2e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java @@ -42,7 +42,6 @@ import android.view.InsetsState; import android.view.ScrollCaptureResponse; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; -import android.view.SurfaceSession; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; @@ -311,7 +310,7 @@ public class SystemWindows { @Override protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) { - SurfaceControl leash = new SurfaceControl.Builder(new SurfaceSession()) + SurfaceControl leash = new SurfaceControl.Builder() .setContainerLayer() .setName("SystemWindowLeash") .setHidden(false) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java index 43c92cab6a68..43f9cb984322 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java @@ -32,7 +32,7 @@ import android.util.ArraySet; import android.view.Surface; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellInit; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhonePipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhonePipKeepClearAlgorithm.java index 133242d15822..a27caf879e8a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhonePipKeepClearAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhonePipKeepClearAlgorithm.java @@ -57,6 +57,12 @@ public class PhonePipKeepClearAlgorithm implements PipKeepClearAlgorithmInterfac Rect startingBounds = pipBoundsState.getBounds().isEmpty() ? pipBoundsAlgorithm.getEntryDestinationBoundsIgnoringKeepClearAreas() : pipBoundsState.getBounds(); + // If IME is not showing and restore bounds (pre-IME bounds) is not empty, we should set PiP + // bounds to the restore bounds. + if (!pipBoundsState.isImeShowing() && !pipBoundsState.getRestoreBounds().isEmpty()) { + startingBounds.set(pipBoundsState.getRestoreBounds()); + pipBoundsState.clearRestoreBounds(); + } Rect insets = new Rect(); pipBoundsAlgorithm.getInsetBounds(insets); if (pipBoundsState.isImeShowing()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java index 58007b50350b..8e026f04ac31 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java @@ -27,7 +27,7 @@ import android.util.DisplayMetrics; import android.util.Size; import android.view.Gravity; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.protolog.ShellProtoLogGroup; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java index 7ceaaea3962f..c487f7543dcf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java @@ -30,9 +30,10 @@ import android.graphics.Rect; import android.os.RemoteException; import android.util.ArraySet; import android.util.Size; +import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.internal.util.function.TriConsumer; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayLayout; @@ -42,9 +43,7 @@ import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Consumer; @@ -69,26 +68,37 @@ public class PipBoundsState { @Retention(RetentionPolicy.SOURCE) public @interface StashType {} + public static final int NAMED_KCA_LAUNCHER_SHELF = 0; + public static final int NAMED_KCA_TABLETOP_MODE = 1; + + @IntDef(prefix = { "NAMED_KCA_" }, value = { + NAMED_KCA_LAUNCHER_SHELF, + NAMED_KCA_TABLETOP_MODE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface NamedKca {} + private static final String TAG = PipBoundsState.class.getSimpleName(); - private final @NonNull Rect mBounds = new Rect(); - private final @NonNull Rect mMovementBounds = new Rect(); - private final @NonNull Rect mNormalBounds = new Rect(); - private final @NonNull Rect mExpandedBounds = new Rect(); - private final @NonNull Rect mNormalMovementBounds = new Rect(); - private final @NonNull Rect mExpandedMovementBounds = new Rect(); - private final @NonNull PipDisplayLayoutState mPipDisplayLayoutState; + @NonNull private final Rect mBounds = new Rect(); + @NonNull private final Rect mMovementBounds = new Rect(); + @NonNull private final Rect mNormalBounds = new Rect(); + @NonNull private final Rect mExpandedBounds = new Rect(); + @NonNull private final Rect mNormalMovementBounds = new Rect(); + @NonNull private final Rect mExpandedMovementBounds = new Rect(); + @NonNull private final Rect mRestoreBounds = new Rect(); + @NonNull private final PipDisplayLayoutState mPipDisplayLayoutState; private final Point mMaxSize = new Point(); private final Point mMinSize = new Point(); - private final @NonNull Context mContext; + @NonNull private final Context mContext; private float mAspectRatio; private int mStashedState = STASH_TYPE_NONE; private int mStashOffset; - private @Nullable PipReentryState mPipReentryState; + @Nullable private PipReentryState mPipReentryState; private final LauncherState mLauncherState = new LauncherState(); - private final @NonNull SizeSpecSource mSizeSpecSource; - private @Nullable ComponentName mLastPipComponentName; - private final @NonNull MotionBoundsState mMotionBoundsState = new MotionBoundsState(); + @NonNull private final SizeSpecSource mSizeSpecSource; + @Nullable private ComponentName mLastPipComponentName; + @NonNull private final MotionBoundsState mMotionBoundsState = new MotionBoundsState(); private boolean mIsImeShowing; private int mImeHeight; private boolean mIsShelfShowing; @@ -120,12 +130,18 @@ public class PipBoundsState { * as unrestricted keep clear area. Values in this map would be appended to * {@link #getUnrestrictedKeepClearAreas()} and this is meant for internal usage only. */ - private final Map<String, Rect> mNamedUnrestrictedKeepClearAreas = new HashMap<>(); + private final SparseArray<Rect> mNamedUnrestrictedKeepClearAreas = new SparseArray<>(); - private @Nullable Runnable mOnMinimalSizeChangeCallback; - private @Nullable TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback; - private List<Consumer<Rect>> mOnPipExclusionBoundsChangeCallbacks = new ArrayList<>(); - private List<Consumer<Float>> mOnAspectRatioChangedCallbacks = new ArrayList<>(); + @Nullable private Runnable mOnMinimalSizeChangeCallback; + @Nullable private TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback; + private final List<Consumer<Rect>> mOnPipExclusionBoundsChangeCallbacks = new ArrayList<>(); + private final List<Consumer<Float>> mOnAspectRatioChangedCallbacks = new ArrayList<>(); + + /** + * This is used to set the launcher shelf height ahead of non-auto-enter-pip animation, + * to avoid the race condition. See also {@link #NAMED_KCA_LAUNCHER_SHELF}. + */ + public final Rect mCachedLauncherShelfHeightKeepClearArea = new Rect(); // the size of the current bounds relative to the max size spec private float mBoundsScale; @@ -389,6 +405,10 @@ public class PipBoundsState { public void setImeVisibility(boolean imeShowing, int imeHeight) { mIsImeShowing = imeShowing; mImeHeight = imeHeight; + // If IME is showing, save the current PiP bounds in case we need to restore it later. + if (mIsImeShowing) { + mRestoreBounds.set(getBounds()); + } } /** Returns whether the IME is currently showing. */ @@ -396,6 +416,16 @@ public class PipBoundsState { return mIsImeShowing; } + /** Returns the bounds to restore PiP to (bounds before IME was expanded). */ + public Rect getRestoreBounds() { + return mRestoreBounds; + } + + /** Sets mRestoreBounds to (0,0,0,0). */ + public void clearRestoreBounds() { + mRestoreBounds.setEmpty(); + } + /** Returns the IME height. */ public int getImeHeight() { return mImeHeight; @@ -430,17 +460,32 @@ public class PipBoundsState { mUnrestrictedKeepClearAreas.addAll(unrestrictedAreas); } - /** Add a named unrestricted keep clear area. */ - public void addNamedUnrestrictedKeepClearArea(@NonNull String name, Rect unrestrictedArea) { - mNamedUnrestrictedKeepClearAreas.put(name, unrestrictedArea); + /** Set a named unrestricted keep clear area. */ + public void setNamedUnrestrictedKeepClearArea( + @NamedKca int tag, @Nullable Rect unrestrictedArea) { + if (unrestrictedArea == null) { + mNamedUnrestrictedKeepClearAreas.remove(tag); + } else { + mNamedUnrestrictedKeepClearAreas.put(tag, unrestrictedArea); + if (tag == NAMED_KCA_LAUNCHER_SHELF) { + mCachedLauncherShelfHeightKeepClearArea.set(unrestrictedArea); + } + } } - /** Remove a named unrestricted keep clear area. */ - public void removeNamedUnrestrictedKeepClearArea(@NonNull String name) { - mNamedUnrestrictedKeepClearAreas.remove(name); + /** + * Forcefully set the keep-clear-area for launcher shelf height if applicable. + * This is used for entering PiP in button navigation mode to make sure the destination bounds + * calculation includes the shelf height, to avoid race conditions that such callback is sent + * from Launcher after the entering animation is started. + */ + public void mayUseCachedLauncherShelfHeight() { + if (!mCachedLauncherShelfHeightKeepClearArea.isEmpty()) { + setNamedUnrestrictedKeepClearArea( + NAMED_KCA_LAUNCHER_SHELF, mCachedLauncherShelfHeightKeepClearArea); + } } - /** * @return restricted keep clear areas. */ @@ -454,9 +499,12 @@ public class PipBoundsState { */ @NonNull public Set<Rect> getUnrestrictedKeepClearAreas() { - if (mNamedUnrestrictedKeepClearAreas.isEmpty()) return mUnrestrictedKeepClearAreas; + if (mNamedUnrestrictedKeepClearAreas.size() == 0) return mUnrestrictedKeepClearAreas; final Set<Rect> unrestrictedAreas = new ArraySet<>(mUnrestrictedKeepClearAreas); - unrestrictedAreas.addAll(mNamedUnrestrictedKeepClearAreas.values()); + for (int i = 0; i < mNamedUnrestrictedKeepClearAreas.size(); i++) { + final int key = mNamedUnrestrictedKeepClearAreas.keyAt(i); + unrestrictedAreas.add(mNamedUnrestrictedKeepClearAreas.get(key)); + } return unrestrictedAreas; } @@ -488,6 +536,10 @@ public class PipBoundsState { /** Set whether the user has resized the PIP. */ public void setHasUserResizedPip(boolean hasUserResizedPip) { mHasUserResizedPip = hasUserResizedPip; + // If user resized PiP while IME is showing, clear the pre-IME restore bounds. + if (hasUserResizedPip && isImeShowing()) { + clearRestoreBounds(); + } } /** Returns whether the user has moved the PIP. */ @@ -498,6 +550,10 @@ public class PipBoundsState { /** Set whether the user has moved the PIP. */ public void setHasUserMovedPip(boolean hasUserMovedPip) { mHasUserMovedPip = hasUserMovedPip; + // If user moved PiP while IME is showing, clear the pre-IME restore bounds. + if (hasUserMovedPip && isImeShowing()) { + clearRestoreBounds(); + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipPerfHintController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipPerfHintController.java index c421dec025f2..b9c698e5d8b4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipPerfHintController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipPerfHintController.java @@ -26,7 +26,7 @@ import android.window.SystemPerformanceHinter.HighPerfSession; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.shared.annotations.ShellMainThread; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt index a09720dd6a70..7070ce99b24c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt @@ -24,16 +24,16 @@ import android.content.Context import android.content.pm.PackageManager import android.graphics.Rect import android.os.RemoteException -import android.os.SystemProperties import android.util.DisplayMetrics import android.util.Log import android.util.Pair import android.util.TypedValue import android.window.TaskSnapshot -import com.android.internal.protolog.common.ProtoLog +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.Flags import com.android.wm.shell.protolog.ShellProtoLogGroup import kotlin.math.abs +import kotlin.math.roundToInt /** A class that includes convenience methods. */ object PipUtils { @@ -149,16 +149,16 @@ object PipUtils { val appBoundsAspRatio = appBounds.width().toFloat() / appBounds.height() val width: Int val height: Int - var left = 0 - var top = 0 + var left = appBounds.left + var top = appBounds.top if (appBoundsAspRatio < aspectRatio) { width = appBounds.width() - height = Math.round(width / aspectRatio) - top = (appBounds.height() - height) / 2 + height = (width / aspectRatio).roundToInt() + top = appBounds.top + (appBounds.height() - height) / 2 } else { height = appBounds.height() - width = Math.round(height * aspectRatio) - left = (appBounds.width() - width) / 2 + width = (height * aspectRatio).roundToInt() + left = appBounds.left + (appBounds.width() - width) / 2 } return Rect(left, top, left + width, top + height) } @@ -177,9 +177,7 @@ object PipUtils { "org.chromium.arc", 0) val isTv = AppGlobals.getPackageManager().hasSystemFeature( PackageManager.FEATURE_LEANBACK, 0) - isPip2ExperimentEnabled = SystemProperties.getBoolean( - "persist.wm_shell.pip2", false) || - (Flags.enablePip2Implementation() && !isArc && !isTv) + isPip2ExperimentEnabled = Flags.enablePip2() && !isArc && !isTv } return isPip2ExperimentEnabled as Boolean } 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 999da2443248..bdbd4cfef7f5 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 @@ -32,7 +32,7 @@ import android.util.Property; import android.view.View; import com.android.wm.shell.R; -import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.shared.animation.Interpolators; /** * View for the handle in the docked stack divider. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java index bc6ed1f63c8a..f7f45ae36eda 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java @@ -16,26 +16,20 @@ package com.android.wm.shell.common.split; -import static android.view.WindowManager.DOCKED_INVALID; import static android.view.WindowManager.DOCKED_LEFT; import static android.view.WindowManager.DOCKED_RIGHT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_30_70; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_70_30; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_MINIMIZE; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_NONE; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_START_AND_DISMISS; -import static com.android.wm.shell.common.split.SplitScreenConstants.SnapPosition; - -import android.content.Context; -import android.content.res.Configuration; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_30_70; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_70_30; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_MINIMIZE; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_START_AND_DISMISS; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SnapPosition; + import android.content.res.Resources; import android.graphics.Rect; -import android.hardware.display.DisplayManager; -import android.view.Display; -import android.view.DisplayInfo; import androidx.annotation.Nullable; @@ -86,7 +80,7 @@ public class DividerSnapAlgorithm { private final float mFixedRatio; /** Allows split ratios to calculated dynamically instead of using {@link #mFixedRatio}. */ private final boolean mAllowFlexibleSplitRatios; - private boolean mIsHorizontalDivision; + private final boolean mIsHorizontalDivision; /** The first target which is still splitting the screen */ private final SnapTarget mFirstSplitTarget; @@ -98,27 +92,6 @@ public class DividerSnapAlgorithm { private final SnapTarget mDismissEndTarget; private final SnapTarget mMiddleTarget; - public static DividerSnapAlgorithm create(Context ctx, Rect insets) { - DisplayInfo displayInfo = new DisplayInfo(); - ctx.getSystemService(DisplayManager.class).getDisplay( - Display.DEFAULT_DISPLAY).getDisplayInfo(displayInfo); - int dividerWindowWidth = ctx.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.docked_stack_divider_thickness); - int dividerInsets = ctx.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.docked_stack_divider_insets); - return new DividerSnapAlgorithm(ctx.getResources(), - displayInfo.logicalWidth, displayInfo.logicalHeight, - dividerWindowWidth - 2 * dividerInsets, - ctx.getApplicationContext().getResources().getConfiguration().orientation - == Configuration.ORIENTATION_PORTRAIT, - insets); - } - - public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, - boolean isHorizontalDivision, Rect insets) { - this(res, displayWidth, displayHeight, dividerSize, isHorizontalDivision, insets, - DOCKED_INVALID, false /* minimized */, true /* resizable */); - } public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, boolean isHorizontalDivision, Rect insets, int dockSide) { @@ -160,29 +133,11 @@ public class DividerSnapAlgorithm { } /** - * @return whether it's feasible to enable split screen in the current configuration, i.e. when - * snapping in the middle both tasks are larger than the minimal task size. - */ - public boolean isSplitScreenFeasible() { - int statusBarSize = mInsets.top; - int navBarSize = mIsHorizontalDivision ? mInsets.bottom : mInsets.right; - int size = mIsHorizontalDivision - ? mDisplayHeight - : mDisplayWidth; - int availableSpace = size - navBarSize - statusBarSize - mDividerSize; - return availableSpace / 2 >= mMinimalSizeResizableTask; - } - - public SnapTarget calculateSnapTarget(int position, float velocity) { - return calculateSnapTarget(position, velocity, true /* hardDismiss */); - } - - /** * @param position the top/left position of the divider * @param velocity current dragging velocity - * @param hardDismiss if set, make it a bit harder to get reach the dismiss targets + * @param hardToDismiss if set, make it a bit harder to get reach the dismiss targets */ - public SnapTarget calculateSnapTarget(int position, float velocity, boolean hardDismiss) { + public SnapTarget calculateSnapTarget(int position, float velocity, boolean hardToDismiss) { if (position < mFirstSplitTarget.position && velocity < -mMinDismissVelocityPxPerSecond) { return mDismissStartTarget; } @@ -190,7 +145,7 @@ public class DividerSnapAlgorithm { return mDismissEndTarget; } if (Math.abs(velocity) < mMinFlingVelocityPxPerSecond) { - return snap(position, hardDismiss); + return snap(position, hardToDismiss); } if (velocity < 0) { return mFirstSplitTarget; @@ -236,19 +191,6 @@ public class DividerSnapAlgorithm { return 0f; } - public SnapTarget getClosestDismissTarget(int position) { - if (position < mFirstSplitTarget.position) { - return mDismissStartTarget; - } else if (position > mLastSplitTarget.position) { - return mDismissEndTarget; - } else if (position - mDismissStartTarget.position - < mDismissEndTarget.position - position) { - return mDismissStartTarget; - } else { - return mDismissEndTarget; - } - } - public SnapTarget getFirstSplitTarget() { return mFirstSplitTarget; } @@ -293,7 +235,7 @@ public class DividerSnapAlgorithm { private SnapTarget snap(int position, boolean hardDismiss) { if (shouldApplyFreeSnapMode(position)) { - return new SnapTarget(position, position, SNAP_TO_NONE); + return new SnapTarget(position, SNAP_TO_NONE); } int minIndex = -1; float minDistance = Float.MAX_VALUE; @@ -321,7 +263,7 @@ public class DividerSnapAlgorithm { if (dockedSide == DOCKED_RIGHT) { startPos += mInsets.left; } - mTargets.add(new SnapTarget(startPos, startPos, SNAP_TO_START_AND_DISMISS, 0.35f)); + mTargets.add(new SnapTarget(startPos, SNAP_TO_START_AND_DISMISS, 0.35f)); switch (mSnapMode) { case SNAP_MODE_16_9: addRatio16_9Targets(isHorizontalDivision, dividerMax); @@ -336,7 +278,7 @@ public class DividerSnapAlgorithm { addMinimizedTarget(isHorizontalDivision, dockedSide); break; } - mTargets.add(new SnapTarget(dividerMax, dividerMax, SNAP_TO_END_AND_DISMISS, 0.35f)); + mTargets.add(new SnapTarget(dividerMax, SNAP_TO_END_AND_DISMISS, 0.35f)); } private void addNonDismissingTargets(boolean isHorizontalDivision, int topPosition, @@ -383,14 +325,14 @@ public class DividerSnapAlgorithm { */ private void maybeAddTarget(int position, int smallerSize, @SnapPosition int snapPosition) { if (smallerSize >= mMinimalSizeResizableTask) { - mTargets.add(new SnapTarget(position, position, snapPosition)); + mTargets.add(new SnapTarget(position, snapPosition)); } } private void addMiddleTarget(boolean isHorizontalDivision) { int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision, mInsets, mDisplayWidth, mDisplayHeight, mDividerSize); - mTargets.add(new SnapTarget(position, position, SNAP_TO_50_50)); + mTargets.add(new SnapTarget(position, SNAP_TO_50_50)); } private void addMinimizedTarget(boolean isHorizontalDivision, int dockedSide) { @@ -404,29 +346,13 @@ public class DividerSnapAlgorithm { position = mDisplayWidth - position - mInsets.right - mDividerSize; } } - mTargets.add(new SnapTarget(position, position, SNAP_TO_MINIMIZE)); + mTargets.add(new SnapTarget(position, SNAP_TO_MINIMIZE)); } public SnapTarget getMiddleTarget() { return mMiddleTarget; } - public SnapTarget getNextTarget(SnapTarget snapTarget) { - int index = mTargets.indexOf(snapTarget); - if (index != -1 && index < mTargets.size() - 1) { - return mTargets.get(index + 1); - } - return snapTarget; - } - - public SnapTarget getPreviousTarget(SnapTarget snapTarget) { - int index = mTargets.indexOf(snapTarget); - if (index != -1 && index > 0) { - return mTargets.get(index - 1); - } - return snapTarget; - } - /** * @return whether or not there are more than 1 split targets that do not include the two * dismiss targets, used in deciding to display the middle target for accessibility @@ -451,40 +377,15 @@ public class DividerSnapAlgorithm { } /** - * Cycles through all non-dismiss targets with a stepping of {@param increment}. It moves left - * if {@param increment} is negative and moves right otherwise. - */ - public SnapTarget cycleNonDismissTarget(SnapTarget snapTarget, int increment) { - int index = mTargets.indexOf(snapTarget); - if (index != -1) { - SnapTarget newTarget = mTargets.get((index + mTargets.size() + increment) - % mTargets.size()); - if (newTarget == mDismissStartTarget) { - return mLastSplitTarget; - } else if (newTarget == mDismissEndTarget) { - return mFirstSplitTarget; - } else { - return newTarget; - } - } - return snapTarget; - } - - /** - * Represents a snap target for the divider. + * An object, calculated at boot time, representing a legal position for the split screen + * divider (i.e. the divider can be dragged to this spot). */ public static class SnapTarget { /** Position of this snap target. The right/bottom edge of the top/left task snaps here. */ public final int position; /** - * Like {@link #position}, but used to calculate the task bounds which might be different - * from the stack bounds. - */ - public final int taskPosition; - - /** - * An int describing the placement of the divider in this snap target. + * An int (enum) describing the placement of the divider in this snap target. */ public final @SnapPosition int snapPosition; @@ -496,14 +397,13 @@ public class DividerSnapAlgorithm { */ private final float distanceMultiplier; - public SnapTarget(int position, int taskPosition, @SnapPosition int snapPosition) { - this(position, taskPosition, snapPosition, 1f); + public SnapTarget(int position, @SnapPosition int snapPosition) { + this(position, snapPosition, 1f); } - public SnapTarget(int position, int taskPosition, @SnapPosition int snapPosition, + public SnapTarget(int position, @SnapPosition int snapPosition, float distanceMultiplier) { this.position = position; - this.taskPosition = taskPosition; this.snapPosition = snapPosition; this.distanceMultiplier = distanceMultiplier; } 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 c2242a8b87fa..e7848e27d7ed 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 @@ -54,10 +54,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; -import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.animation.Interpolators; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; /** * Divider for multi window splits. @@ -228,7 +229,9 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { : R.dimen.split_divider_handle_region_width); mHandleRegionHeight = getResources().getDimensionPixelSize(isLeftRightSplit ? R.dimen.split_divider_handle_region_width - : R.dimen.split_divider_handle_region_height); + : DesktopModeStatus.canEnterDesktopMode(mContext) + ? R.dimen.desktop_mode_portrait_split_divider_handle_region_height + : R.dimen.split_divider_handle_region_height); } void onInsetsChanged(InsetsState insetsState, boolean animate) { @@ -336,11 +339,6 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { setTouching(); mStartPos = touchPos; mMoving = false; - // This triggers initialization of things like the resize veil in preparation for - // showing it when the user moves the divider past the slop, and has to be done - // before onStartDragging() which starts the jank interaction tracing - mSplitLayout.updateDividerBounds(mSplitLayout.getDividerPosition(), - false /* shouldUseParallaxEffect */); mSplitLayout.onStartDragging(); break; case MotionEvent.ACTION_MOVE: @@ -497,6 +495,11 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { return mHideHandle; } + /** Returns true if the divider is currently being physically controlled by the user. */ + boolean isMoving() { + return mMoving; + } + private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDoubleTap(MotionEvent e) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java index 5097ed8866c9..de3152ad7687 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java @@ -23,7 +23,10 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; -import static com.android.wm.shell.common.split.SplitScreenConstants.FADE_DURATION; +import static com.android.wm.shell.common.split.SplitLayout.BEHIND_APP_VEIL_LAYER; +import static com.android.wm.shell.common.split.SplitLayout.FRONT_APP_VEIL_LAYER; +import static com.android.wm.shell.shared.split.SplitScreenConstants.FADE_DURATION; +import static com.android.wm.shell.shared.split.SplitScreenConstants.VEIL_DELAY_DURATION; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -31,6 +34,7 @@ import android.animation.ValueAnimator; import android.app.ActivityManager; import android.content.Context; import android.content.res.Configuration; +import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -39,7 +43,6 @@ import android.view.IWindow; import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; -import android.view.SurfaceSession; import android.view.View; import android.view.WindowManager; import android.view.WindowlessWindowManager; @@ -70,10 +73,9 @@ public class SplitDecorManager extends WindowlessWindowManager { private static final String GAP_BACKGROUND_SURFACE_NAME = "GapBackground"; private final IconProvider mIconProvider; - private final SurfaceSession mSurfaceSession; private Drawable mIcon; - private ImageView mResizingIconView; + private ImageView mVeilIconView; private SurfaceControlViewHost mViewHost; private SurfaceControl mHostLeash; private SurfaceControl mIconLeash; @@ -82,13 +84,14 @@ public class SplitDecorManager extends WindowlessWindowManager { private SurfaceControl mScreenshot; private boolean mShown; - private boolean mIsResizing; + /** True if the task is going through some kind of transition (moving or changing size). */ + private boolean mIsCurrentlyChanging; /** The original bounds of the main task, captured at the beginning of a resize transition. */ private final Rect mOldMainBounds = new Rect(); /** The original bounds of the side task, captured at the beginning of a resize transition. */ private final Rect mOldSideBounds = new Rect(); /** The current bounds of the main task, mid-resize. */ - private final Rect mResizingBounds = new Rect(); + private final Rect mInstantaneousBounds = new Rect(); private final Rect mTempRect = new Rect(); private ValueAnimator mFadeAnimator; private ValueAnimator mScreenshotAnimator; @@ -98,17 +101,15 @@ public class SplitDecorManager extends WindowlessWindowManager { private int mOffsetY; private int mRunningAnimationCount = 0; - public SplitDecorManager(Configuration configuration, IconProvider iconProvider, - SurfaceSession surfaceSession) { + public SplitDecorManager(Configuration configuration, IconProvider iconProvider) { super(configuration, null /* rootSurface */, null /* hostInputToken */); mIconProvider = iconProvider; - mSurfaceSession = surfaceSession; } @Override protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) { // Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later. - final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + final SurfaceControl.Builder builder = new SurfaceControl.Builder() .setContainerLayer() .setName(TAG) .setHidden(true) @@ -133,7 +134,7 @@ public class SplitDecorManager extends WindowlessWindowManager { mIconSize = context.getResources().getDimensionPixelSize(R.dimen.split_icon_size); final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(context) .inflate(R.layout.split_decor, null); - mResizingIconView = rootLayout.findViewById(R.id.split_resizing_icon); + mVeilIconView = rootLayout.findViewById(R.id.split_resizing_icon); final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( 0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY, @@ -190,28 +191,28 @@ public class SplitDecorManager extends WindowlessWindowManager { } mHostLeash = null; mIcon = null; - mResizingIconView = null; - mIsResizing = false; + mVeilIconView = null; + mIsCurrentlyChanging = false; mShown = false; mOldMainBounds.setEmpty(); mOldSideBounds.setEmpty(); - mResizingBounds.setEmpty(); + mInstantaneousBounds.setEmpty(); } /** Showing resizing hint. */ public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t, int offsetX, int offsetY, - boolean immediately, float[] veilColor) { - if (mResizingIconView == null) { + boolean immediately) { + if (mVeilIconView == null) { return; } - if (!mIsResizing) { - mIsResizing = true; + if (!mIsCurrentlyChanging) { + mIsCurrentlyChanging = true; mOldMainBounds.set(newBounds); mOldSideBounds.set(sideBounds); } - mResizingBounds.set(newBounds); + mInstantaneousBounds.set(newBounds); mOffsetX = offsetX; mOffsetY = offsetY; @@ -233,8 +234,8 @@ public class SplitDecorManager extends WindowlessWindowManager { if (mBackgroundLeash == null) { mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash, - RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession); - t.setColor(mBackgroundLeash, veilColor) + RESIZING_BACKGROUND_SURFACE_NAME); + t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask)) .setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1); } @@ -243,9 +244,9 @@ public class SplitDecorManager extends WindowlessWindowManager { final int left = isLandscape ? mOldMainBounds.width() : 0; final int top = isLandscape ? 0 : mOldMainBounds.height(); mGapBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash, - GAP_BACKGROUND_SURFACE_NAME, mSurfaceSession); + GAP_BACKGROUND_SURFACE_NAME); // Fill up another side bounds area. - t.setColor(mGapBackgroundLeash, veilColor) + t.setColor(mGapBackgroundLeash, getResizingBackgroundColor(resizingTask)) .setLayer(mGapBackgroundLeash, Integer.MAX_VALUE - 2) .setPosition(mGapBackgroundLeash, left, top) .setWindowCrop(mGapBackgroundLeash, sideBounds.width(), sideBounds.height()); @@ -253,8 +254,8 @@ public class SplitDecorManager extends WindowlessWindowManager { if (mIcon == null && resizingTask.topActivityInfo != null) { mIcon = mIconProvider.getIcon(resizingTask.topActivityInfo); - mResizingIconView.setImageDrawable(mIcon); - mResizingIconView.setVisibility(View.VISIBLE); + mVeilIconView.setImageDrawable(mIcon); + mVeilIconView.setVisibility(View.VISIBLE); WindowManager.LayoutParams lp = (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams(); @@ -274,7 +275,12 @@ public class SplitDecorManager extends WindowlessWindowManager { t.setAlpha(mIconLeash, showVeil ? 1f : 0f); t.setVisibility(mIconLeash, showVeil); } else { - startFadeAnimation(showVeil, false, null); + startFadeAnimation( + showVeil, + false /* releaseSurface */, + null /* finishedCallback */, + false /* addDelay */ + ); } mShown = showVeil; } @@ -319,19 +325,19 @@ public class SplitDecorManager extends WindowlessWindowManager { mScreenshotAnimator.start(); } - if (mResizingIconView == null) { + if (mVeilIconView == null) { if (mRunningAnimationCount == 0 && animFinishedCallback != null) { animFinishedCallback.accept(false); } return; } - mIsResizing = false; + mIsCurrentlyChanging = false; mOffsetX = 0; mOffsetY = 0; mOldMainBounds.setEmpty(); mOldSideBounds.setEmpty(); - mResizingBounds.setEmpty(); + mInstantaneousBounds.setEmpty(); if (mFadeAnimator != null && mFadeAnimator.isRunning()) { if (!mShown) { // If fade-out animation is running, just add release callback to it. @@ -355,7 +361,7 @@ public class SplitDecorManager extends WindowlessWindowManager { if (mRunningAnimationCount == 0 && animFinishedCallback != null) { animFinishedCallback.accept(true); } - }); + }, false /* addDelay */); } else { // Decor surface is hidden so release it directly. releaseDecor(t); @@ -365,9 +371,94 @@ public class SplitDecorManager extends WindowlessWindowManager { } } + /** + * Called (on every frame) when two split apps are swapping, and a veil is needed. + */ + public void drawNextVeilFrameForSwapAnimation(ActivityManager.RunningTaskInfo resizingTask, + Rect newBounds, SurfaceControl.Transaction t, boolean isGoingBehind, + SurfaceControl leash, float iconOffsetX, float iconOffsetY) { + if (mVeilIconView == null) { + return; + } + + if (!mIsCurrentlyChanging) { + mIsCurrentlyChanging = true; + } + + mInstantaneousBounds.set(newBounds); + mOffsetX = (int) iconOffsetX; + mOffsetY = (int) iconOffsetY; + + t.setLayer(leash, isGoingBehind ? BEHIND_APP_VEIL_LAYER : FRONT_APP_VEIL_LAYER); + + if (!mShown) { + if (mFadeAnimator != null && mFadeAnimator.isRunning()) { + // Cancel mFadeAnimator if it is running + mFadeAnimator.cancel(); + } + } + + if (mBackgroundLeash == null) { + // Initialize background + mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash, + RESIZING_BACKGROUND_SURFACE_NAME); + t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask)) + .setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1); + } + + if (mIcon == null && resizingTask.topActivityInfo != null) { + // Initialize icon + mIcon = mIconProvider.getIcon(resizingTask.topActivityInfo); + mVeilIconView.setImageDrawable(mIcon); + mVeilIconView.setVisibility(View.VISIBLE); + + WindowManager.LayoutParams lp = + (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams(); + lp.width = mIconSize; + lp.height = mIconSize; + mViewHost.relayout(lp); + + t.setLayer(mIconLeash, Integer.MAX_VALUE); + } + + t.setPosition(mIconLeash, + newBounds.width() / 2 - mIconSize / 2 - mOffsetX, + newBounds.height() / 2 - mIconSize / 2 - mOffsetY); + + // If this is the first frame, we need to trigger the veil's fade-in animation. + if (!mShown) { + startFadeAnimation( + true /* show */, + false /* releaseSurface */, + null /* finishedCallball */, + false /* addDelay */ + ); + mShown = true; + } + } + + /** Called at the end of the swap animation. */ + public void fadeOutVeilAndCleanUp(SurfaceControl.Transaction t) { + if (mVeilIconView == null) { + return; + } + + // Recenter icon + t.setPosition(mIconLeash, + mInstantaneousBounds.width() / 2f - mIconSize / 2f, + mInstantaneousBounds.height() / 2f - mIconSize / 2f); + + mIsCurrentlyChanging = false; + mOffsetX = 0; + mOffsetY = 0; + mInstantaneousBounds.setEmpty(); + + fadeOutDecor(() -> {}, true /* addDelay */); + } + /** Screenshot host leash and attach on it if meet some conditions */ public void screenshotIfNeeded(SurfaceControl.Transaction t) { - if (!mShown && mIsResizing && !mOldMainBounds.equals(mResizingBounds)) { + if (!mShown && mIsCurrentlyChanging && !mOldMainBounds.equals(mInstantaneousBounds)) { if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { mScreenshotAnimator.cancel(); } else if (mScreenshot != null) { @@ -385,7 +476,7 @@ public class SplitDecorManager extends WindowlessWindowManager { public void setScreenshotIfNeeded(SurfaceControl screenshot, SurfaceControl.Transaction t) { if (screenshot == null || !screenshot.isValid()) return; - if (!mShown && mIsResizing && !mOldMainBounds.equals(mResizingBounds)) { + if (!mShown && mIsCurrentlyChanging && !mOldMainBounds.equals(mInstantaneousBounds)) { if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { mScreenshotAnimator.cancel(); } else if (mScreenshot != null) { @@ -400,24 +491,35 @@ public class SplitDecorManager extends WindowlessWindowManager { /** Fade-out decor surface with animation end callback, if decor is hidden, run the callback * directly. */ - public void fadeOutDecor(Runnable finishedCallback) { + public void fadeOutDecor(Runnable finishedCallback, boolean addDelay) { if (mShown) { // If previous animation is running, just cancel it. if (mFadeAnimator != null && mFadeAnimator.isRunning()) { mFadeAnimator.cancel(); } - startFadeAnimation(false /* show */, true, finishedCallback); + startFadeAnimation( + false /* show */, true /* releaseSurface */, finishedCallback, addDelay); mShown = false; } else { if (finishedCallback != null) finishedCallback.run(); } } + /** + * Fades the veil in or out. Called at the first frame of a movement or resize when a veil is + * needed (with show = true), and called again at the end (with show = false). + * @param addDelay If true, adds a short delay before fading out to get the app behind the veil + * time to redraw. + */ private void startFadeAnimation(boolean show, boolean releaseSurface, - Runnable finishedCallback) { + Runnable finishedCallback, boolean addDelay) { final SurfaceControl.Transaction animT = new SurfaceControl.Transaction(); + mFadeAnimator = ValueAnimator.ofFloat(0f, 1f); + if (addDelay) { + mFadeAnimator.setStartDelay(VEIL_DELAY_DURATION); + } mFadeAnimator.setDuration(FADE_DURATION); mFadeAnimator.addUpdateListener(valueAnimator-> { final float progress = (float) valueAnimator.getAnimatedValue(); @@ -480,10 +582,15 @@ public class SplitDecorManager extends WindowlessWindowManager { } if (mIcon != null) { - mResizingIconView.setVisibility(View.GONE); - mResizingIconView.setImageDrawable(null); + mVeilIconView.setVisibility(View.GONE); + mVeilIconView.setImageDrawable(null); t.hide(mIconLeash); mIcon = null; } } + + private static float[] getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) { + final int taskBgColor = taskInfo.taskDescription.getBackgroundColor(); + return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).getComponents(); + } } 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 8ced76fd23af..4b55fd0f5527 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 @@ -26,13 +26,15 @@ import static android.view.WindowManager.DOCKED_TOP; import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER; import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE; -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.SplitScreenConstants.SNAP_TO_END_AND_DISMISS; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_START_AND_DISMISS; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR; +import static com.android.wm.shell.shared.animation.Interpolators.EMPHASIZED; +import static com.android.wm.shell.shared.animation.Interpolators.LINEAR; +import static com.android.wm.shell.shared.animation.Interpolators.SLOWDOWN_INTERPOLATOR; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_START_AND_DISMISS; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER; import android.animation.Animator; @@ -46,6 +48,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; +import android.os.Handler; import android.view.Display; import android.view.InsetsSourceControl; import android.view.InsetsState; @@ -53,25 +56,31 @@ import android.view.RoundedCorner; import android.view.SurfaceControl; import android.view.WindowInsets; import android.view.WindowManager; +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.jank.InteractionJankMonitor; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.animation.Interpolators; 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.DisplayLayout; -import com.android.wm.shell.common.InteractionJankMonitorUtils; -import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; -import com.android.wm.shell.common.split.SplitScreenConstants.SnapPosition; -import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; +import com.android.wm.shell.common.pip.PipUtils; +import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.animation.Interpolators; +import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; +import com.android.wm.shell.shared.split.SplitScreenConstants.SnapPosition; +import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; +import com.android.wm.shell.splitscreen.StageTaskListener; import java.io.PrintWriter; import java.util.function.Consumer; @@ -87,10 +96,31 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public static final int PARALLAX_ALIGN_CENTER = 2; public static final int FLING_RESIZE_DURATION = 250; - private static final int FLING_SWITCH_DURATION = 350; private static final int FLING_ENTER_DURATION = 450; private static final int FLING_EXIT_DURATION = 450; + // Here are some (arbitrarily decided) layer definitions used during animations to make sure the + // layers stay in order. Note: This does not affect any other layer numbering systems because + // the layer system in WindowManager is local within sibling groups. So, for example, each + // "veil layer" defined here actually has two sub-layers; and *their* layer values, which we set + // in SplitDecorManager, are only important relative to each other. + public static final int DIVIDER_LAYER = 0; + public static final int FRONT_APP_VEIL_LAYER = DIVIDER_LAYER + 20; + public static final int FRONT_APP_LAYER = DIVIDER_LAYER + 10; + public static final int BEHIND_APP_VEIL_LAYER = DIVIDER_LAYER - 10; + public static final int BEHIND_APP_LAYER = DIVIDER_LAYER - 20; + + // Animation specs for the swap animation + private static final int SWAP_ANIMATION_TOTAL_DURATION = 500; + private static final float SWAP_ANIMATION_SHRINK_DURATION = 83; + private static final float SWAP_ANIMATION_SHRINK_MARGIN_DP = 14; + private static final Interpolator SHRINK_INTERPOLATOR = + new PathInterpolator(0.2f, 0f, 0f, 1f); + private static final Interpolator GROW_INTERPOLATOR = + new PathInterpolator(0.45f, 0f, 0.5f, 1f); + @ShellMainThread + private final Handler mHandler; + private int mDividerWindowWidth; private int mDividerInsets; private int mDividerSize; @@ -131,14 +161,17 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private final boolean mDimNonImeSide; private final boolean mAllowLeftRightSplitInPortrait; + private final InteractionJankMonitor mInteractionJankMonitor; private boolean mIsLeftRightSplit; private ValueAnimator mDividerFlingAnimator; + private AnimatorSet mSwapAnimator; public SplitLayout(String windowName, Context context, Configuration configuration, SplitLayoutHandler splitLayoutHandler, SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks, DisplayController displayController, DisplayImeController displayImeController, - ShellTaskOrganizer taskOrganizer, int parallaxType) { + ShellTaskOrganizer taskOrganizer, int parallaxType, @ShellMainThread Handler handler) { + mHandler = handler; mContext = context.createConfigurationContext(configuration); mOrientation = configuration.orientation; mRotation = configuration.windowConfiguration.getRotation(); @@ -163,6 +196,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mRootBounds.set(configuration.windowConfiguration.getBounds()); mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); + mInteractionJankMonitor = InteractionJankMonitor.getInstance(); resetDividerPosition(); updateInvisibleRect(); } @@ -515,7 +549,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * to middle position if the provided SnapTarget is not supported. */ public void setDivideRatio(@PersistentSnapPosition int snapPosition) { - final DividerSnapAlgorithm.SnapTarget snapTarget = mDividerSnapAlgorithm.findSnapTarget( + final SnapTarget snapTarget = mDividerSnapAlgorithm.findSnapTarget( snapPosition); setDividerPosition(snapTarget != null @@ -549,7 +583,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * Sets new divider position and updates bounds correspondingly. Notifies listener if the new * target indicates dismissing split. */ - public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) { + public void snapToTarget(int currentPosition, SnapTarget snapTarget) { switch (snapTarget.snapPosition) { case SNAP_TO_START_AND_DISMISS: flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, @@ -569,23 +603,27 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } void onStartDragging() { - InteractionJankMonitorUtils.beginTracing(CUJ_SPLIT_SCREEN_RESIZE, mContext, - getDividerLeash(), null /* tag */); + mInteractionJankMonitor.begin(getDividerLeash(), mContext, mHandler, + CUJ_SPLIT_SCREEN_RESIZE); } void onDraggingCancelled() { - InteractionJankMonitorUtils.cancelTracing(CUJ_SPLIT_SCREEN_RESIZE); + mInteractionJankMonitor.cancel(CUJ_SPLIT_SCREEN_RESIZE); } void onDoubleTappedDivider() { + if (isCurrentlySwapping()) { + return; + } + mSplitLayoutHandler.onDoubleTappedDivider(); } /** - * Returns {@link DividerSnapAlgorithm.SnapTarget} which matches passing position and velocity. + * Returns {@link SnapTarget} which matches passing position and velocity. * If hardDismiss is set to {@code true}, it will be harder to reach dismiss target. */ - public DividerSnapAlgorithm.SnapTarget findSnapTarget(int position, float velocity, + public SnapTarget findSnapTarget(int position, float velocity, boolean hardDismiss) { return mDividerSnapAlgorithm.calculateSnapTarget(position, velocity, hardDismiss); } @@ -638,7 +676,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange if (flingFinishedCallback != null) { flingFinishedCallback.run(); } - InteractionJankMonitorUtils.endTracing( + mInteractionJankMonitor.end( CUJ_SPLIT_SCREEN_RESIZE); return; } @@ -651,9 +689,18 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange .ofInt(from, to) .setDuration(duration); mDividerFlingAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); + + // If the divider is being physically controlled by the user, we use a cool parallax effect + // on the task windows. So if this "snap" animation is an extension of a user-controlled + // movement, we pass in true here to continue the parallax effect smoothly. + boolean isBeingMovedByUser = mSplitWindowManager.getDividerView() != null + && mSplitWindowManager.getDividerView().isMoving(); + mDividerFlingAnimator.addUpdateListener( animation -> updateDividerBounds( - (int) animation.getAnimatedValue(), false /* shouldUseParallaxEffect */) + (int) animation.getAnimatedValue(), + isBeingMovedByUser /* shouldUseParallaxEffect */ + ) ); mDividerFlingAnimator.addListener(new AnimatorListenerAdapter() { @Override @@ -661,7 +708,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange if (flingFinishedCallback != null) { flingFinishedCallback.run(); } - InteractionJankMonitorUtils.endTracing( + mInteractionJankMonitor.end( CUJ_SPLIT_SCREEN_RESIZE); mDividerFlingAnimator = null; } @@ -675,40 +722,47 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } /** Switch both surface position with animation. */ - public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1, - SurfaceControl leash2, Consumer<Rect> finishCallback) { + public void playSwapAnimation(SurfaceControl.Transaction t, StageTaskListener topLeftStage, + StageTaskListener bottomRightStage, Consumer<Rect> finishCallback) { final Rect insets = getDisplayStableInsets(mContext); + // If we have insets in the direction of the swap, the animation won't look correct because + // window contents will shift and redraw again at the end. So we show a veil to hide that. insets.set(mIsLeftRightSplit ? insets.left : 0, mIsLeftRightSplit ? 0 : insets.top, mIsLeftRightSplit ? insets.right : 0, mIsLeftRightSplit ? 0 : insets.bottom); + final boolean shouldVeil = + insets.left != 0 || insets.top != 0 || insets.right != 0 || insets.bottom != 0; final int dividerPos = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget( mIsLeftRightSplit ? mBounds2.width() : mBounds2.height()).position; - final Rect distBounds1 = new Rect(); - final Rect distBounds2 = new Rect(); - final Rect distDividerBounds = new Rect(); - // Compute dist bounds. - updateBounds(dividerPos, distBounds2, distBounds1, distDividerBounds, + final Rect endBounds1 = new Rect(); + final Rect endBounds2 = new Rect(); + final Rect endDividerBounds = new Rect(); + // Compute destination bounds. + updateBounds(dividerPos, endBounds2, endBounds1, endDividerBounds, false /* setEffectBounds */); // Offset to real position under root container. - distBounds1.offset(-mRootBounds.left, -mRootBounds.top); - distBounds2.offset(-mRootBounds.left, -mRootBounds.top); - distDividerBounds.offset(-mRootBounds.left, -mRootBounds.top); - - ValueAnimator animator1 = moveSurface(t, leash1, getRefBounds1(), distBounds1, - -insets.left, -insets.top); - ValueAnimator animator2 = moveSurface(t, leash2, getRefBounds2(), distBounds2, - insets.left, insets.top); - ValueAnimator animator3 = moveSurface(t, getDividerLeash(), getRefDividerBounds(), - distDividerBounds, 0 /* offsetX */, 0 /* offsetY */); - - AnimatorSet set = new AnimatorSet(); - set.playTogether(animator1, animator2, animator3); - set.setDuration(FLING_SWITCH_DURATION); - set.addListener(new AnimatorListenerAdapter() { + endBounds1.offset(-mRootBounds.left, -mRootBounds.top); + endBounds2.offset(-mRootBounds.left, -mRootBounds.top); + endDividerBounds.offset(-mRootBounds.left, -mRootBounds.top); + + ValueAnimator animator1 = moveSurface(t, topLeftStage, getRefBounds1(), endBounds1, + -insets.left, -insets.top, true /* roundCorners */, true /* isGoingBehind */, + shouldVeil); + ValueAnimator animator2 = moveSurface(t, bottomRightStage, getRefBounds2(), endBounds2, + insets.left, insets.top, true /* roundCorners */, false /* isGoingBehind */, + shouldVeil); + ValueAnimator animator3 = moveSurface(t, null /* stage */, getRefDividerBounds(), + endDividerBounds, 0 /* offsetX */, 0 /* offsetY */, false /* roundCorners */, + false /* isGoingBehind */, false /* addVeil */); + + mSwapAnimator = new AnimatorSet(); + mSwapAnimator.playTogether(animator1, animator2, animator3); + mSwapAnimator.setDuration(SWAP_ANIMATION_TOTAL_DURATION); + mSwapAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { - InteractionJankMonitorUtils.beginTracing(CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER, - mContext, getDividerLeash(), null /*tag*/); + mInteractionJankMonitor.begin(getDividerLeash(), + mContext, mHandler, CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER); } @Override @@ -716,44 +770,155 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mDividerPosition = dividerPos; updateBounds(mDividerPosition); finishCallback.accept(insets); - InteractionJankMonitorUtils.endTracing(CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER); + mInteractionJankMonitor.end(CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER); } @Override public void onAnimationCancel(Animator animation) { - InteractionJankMonitorUtils.cancelTracing(CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER); + mInteractionJankMonitor.cancel(CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER); } }); - set.start(); + mSwapAnimator.start(); } - private ValueAnimator moveSurface(SurfaceControl.Transaction t, SurfaceControl leash, - Rect start, Rect end, float offsetX, float offsetY) { + /** Returns true if a swap animation is currently playing. */ + public boolean isCurrentlySwapping() { + return mSwapAnimator != null && mSwapAnimator.isRunning(); + } + + /** + * Animates a task leash across the screen. Currently used only for the swap animation. + * + * @param stage The stage holding the task being animated. If null, it is the divider. + * @param roundCorners Whether we should round the corners of the task while animating. + * @param isGoingBehind Whether we should a shrink-and-grow effect to the task while it is + * moving. (Simulates moving behind the divider.) + */ + private ValueAnimator moveSurface(SurfaceControl.Transaction t, StageTaskListener stage, + Rect start, Rect end, float offsetX, float offsetY, boolean roundCorners, + boolean isGoingBehind, boolean addVeil) { + final boolean isApp = stage != null; // check if this is an app or a divider + final SurfaceControl leash = isApp ? stage.getRootLeash() : getDividerLeash(); + final ActivityManager.RunningTaskInfo taskInfo = isApp ? stage.getRunningTaskInfo() : null; + final SplitDecorManager decorManager = isApp ? stage.getDecorManager() : null; + Rect tempStart = new Rect(start); Rect tempEnd = new Rect(end); final float diffX = tempEnd.left - tempStart.left; final float diffY = tempEnd.top - tempStart.top; final float diffWidth = tempEnd.width() - tempStart.width(); final float diffHeight = tempEnd.height() - tempStart.height(); + + // Get display measurements (for possible shrink animation). + final RoundedCorner roundedCorner = mSplitWindowManager.getDividerView().getDisplay() + .getRoundedCorner(0 /* position */); + float cornerRadius = roundedCorner == null ? 0 : roundedCorner.getRadius(); + float shrinkMarginPx = PipUtils.dpToPx( + SWAP_ANIMATION_SHRINK_MARGIN_DP, mContext.getResources().getDisplayMetrics()); + float shrinkAmountPx = shrinkMarginPx * 2; + + // Timing calculations + float shrinkPortion = SWAP_ANIMATION_SHRINK_DURATION / SWAP_ANIMATION_TOTAL_DURATION; + float growPortion = 1 - shrinkPortion; + ValueAnimator animator = ValueAnimator.ofFloat(0, 1); + // Set the base animation to proceed linearly. Each component of the animation (movement, + // shrinking, growing) overrides it with a different interpolator later. + animator.setInterpolator(LINEAR); animator.addUpdateListener(animation -> { if (leash == null) return; + if (roundCorners) { + // Add rounded corners to the task leash while it is animating. + t.setCornerRadius(leash, cornerRadius); + } + + final float progress = (float) animation.getAnimatedValue(); + final float moveProgress = EMPHASIZED.getInterpolation(progress); + float instantaneousX = tempStart.left + moveProgress * diffX; + float instantaneousY = tempStart.top + moveProgress * diffY; + int width = (int) (tempStart.width() + moveProgress * diffWidth); + int height = (int) (tempStart.height() + moveProgress * diffHeight); + + if (isGoingBehind) { + float shrinkDiffX; // the position adjustments needed for this frame + float shrinkDiffY; + float shrinkScaleX; // the scale adjustments needed for this frame + float shrinkScaleY; + + // Find the max amount we will be shrinking this leash, as a proportion (e.g. 0.1f). + float maxShrinkX = shrinkAmountPx / height; + float maxShrinkY = shrinkAmountPx / width; + + // Find if we are in the shrinking part of the animation, or the growing part. + boolean shrinking = progress <= shrinkPortion; + + if (shrinking) { + // Find how far into the shrink portion we are (e.g. 0.5f). + float shrinkProgress = progress / shrinkPortion; + // Find how much we should have progressed in shrinking the leash (e.g. 0.8f). + float interpolatedShrinkProgress = + SHRINK_INTERPOLATOR.getInterpolation(shrinkProgress); + // Find how much width proportion we should be taking off (e.g. 0.1f) + float widthProportionLost = maxShrinkX * interpolatedShrinkProgress; + shrinkScaleX = 1 - widthProportionLost; + // Find how much height proportion we should be taking off (e.g. 0.1f) + float heightProportionLost = maxShrinkY * interpolatedShrinkProgress; + shrinkScaleY = 1 - heightProportionLost; + // Add a small amount to the leash's position to keep the task centered. + shrinkDiffX = (width * widthProportionLost) / 2; + shrinkDiffY = (height * heightProportionLost) / 2; + } else { + // Find how far into the grow portion we are (e.g. 0.5f). + float growProgress = (progress - shrinkPortion) / growPortion; + // Find how much we should have progressed in growing the leash (e.g. 0.8f). + float interpolatedGrowProgress = + GROW_INTERPOLATOR.getInterpolation(growProgress); + // Find how much width proportion we should be taking off (e.g. 0.1f) + float widthProportionLost = maxShrinkX * (1 - interpolatedGrowProgress); + shrinkScaleX = 1 - widthProportionLost; + // Find how much height proportion we should be taking off (e.g. 0.1f) + float heightProportionLost = maxShrinkY * (1 - interpolatedGrowProgress); + shrinkScaleY = 1 - heightProportionLost; + // Add a small amount to the leash's position to keep the task centered. + shrinkDiffX = (width * widthProportionLost) / 2; + shrinkDiffY = (height * heightProportionLost) / 2; + } + + instantaneousX += shrinkDiffX; + instantaneousY += shrinkDiffY; + width *= shrinkScaleX; + height *= shrinkScaleY; + // Set scale on the leash's contents. + t.setScale(leash, shrinkScaleX, shrinkScaleY); + } + + // Set layers + if (taskInfo != null) { + t.setLayer(leash, isGoingBehind ? BEHIND_APP_LAYER : FRONT_APP_LAYER); + } else { + t.setLayer(leash, DIVIDER_LAYER); + } - final float scale = (float) animation.getAnimatedValue(); - final float distX = tempStart.left + scale * diffX; - final float distY = tempStart.top + scale * diffY; - final int width = (int) (tempStart.width() + scale * diffWidth); - final int height = (int) (tempStart.height() + scale * diffHeight); if (offsetX == 0 && offsetY == 0) { - t.setPosition(leash, distX, distY); + t.setPosition(leash, instantaneousX, instantaneousY); + mTempRect.set((int) instantaneousX, (int) instantaneousY, + (int) (instantaneousX + width), (int) (instantaneousY + height)); t.setWindowCrop(leash, width, height); + if (addVeil) { + decorManager.drawNextVeilFrameForSwapAnimation( + taskInfo, mTempRect, t, isGoingBehind, leash, 0, 0); + } } else { - final int diffOffsetX = (int) (scale * offsetX); - final int diffOffsetY = (int) (scale * offsetY); - t.setPosition(leash, distX + diffOffsetX, distY + diffOffsetY); + final int diffOffsetX = (int) (moveProgress * offsetX); + final int diffOffsetY = (int) (moveProgress * offsetY); + t.setPosition(leash, instantaneousX + diffOffsetX, instantaneousY + diffOffsetY); mTempRect.set(0, 0, width, height); mTempRect.offsetTo(-diffOffsetX, -diffOffsetY); t.setCrop(leash, mTempRect); + if (addVeil) { + decorManager.drawNextVeilFrameForSwapAnimation( + taskInfo, mTempRect, t, isGoingBehind, leash, diffOffsetX, diffOffsetY); + } } t.apply(); }); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java index e8226051b672..bdbcb4635ae8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java @@ -16,18 +16,17 @@ package com.android.wm.shell.common.split; -import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES; -import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.shared.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES; +import static com.android.wm.shell.shared.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import android.app.ActivityManager; import android.app.PendingIntent; import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.Color; import android.graphics.Rect; import androidx.annotation.Nullable; @@ -35,6 +34,7 @@ import androidx.annotation.Nullable; import com.android.internal.util.ArrayUtils; import com.android.wm.shell.Flags; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.shared.split.SplitScreenConstants; /** Helper utility class for split screen components to use. */ public class SplitScreenUtils { @@ -128,10 +128,4 @@ public class SplitScreenUtils { return isLandscape; } } - - /** Returns the specified background color that matches a RunningTaskInfo. */ - public static Color getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) { - final int taskBgColor = taskInfo.taskDescription.getBackgroundColor(); - return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor); - } } 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 5d121c23c6e1..c5f19742c803 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,8 +36,6 @@ import android.view.InsetsState; import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; -import android.view.SurfaceSession; -import android.view.View; import android.view.WindowManager; import android.view.WindowlessWindowManager; @@ -99,7 +97,7 @@ public final class SplitWindowManager extends WindowlessWindowManager { @Override protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) { // Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later. - final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + final SurfaceControl.Builder builder = new SurfaceControl.Builder() .setContainerLayer() .setName(TAG) .setHidden(true) @@ -192,7 +190,7 @@ public final class SplitWindowManager extends WindowlessWindowManager { mDividerView.setInteractive(interactive, hideHandle, from); } - View getDividerView() { + DividerView getDividerView() { return mDividerView; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt index 6781d08c9904..d1b2347a4411 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt @@ -19,6 +19,16 @@ package com.android.wm.shell.compatui import android.app.TaskInfo -fun isSingleTopActivityTranslucent(task: TaskInfo) = - task.isTopActivityTransparent && task.numActivities == 1 +import android.content.Context +import com.android.internal.R +// TODO(b/347289970): Consider replacing with API +fun isTopActivityExemptFromDesktopWindowing(context: Context, task: TaskInfo) = + isSystemUiTask(context, task) || (task.isTopActivityTransparent && task.numActivities == 1 + && !task.isTopActivityStyleFloating) + +private fun isSystemUiTask(context: Context, task: TaskInfo): Boolean { + val sysUiPackageName: String = + context.resources.getString(R.string.config_systemUi) + return task.baseActivity?.packageName == sysUiPackageName +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index 2520c25613e7..6146ecd9ade6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -20,7 +20,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.CameraCompatTaskInfo.CameraCompatControlState; import android.app.TaskInfo; import android.content.ComponentName; import android.content.Context; @@ -40,6 +39,7 @@ import android.view.InsetsState; import android.view.accessibility.AccessibilityManager; import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener; @@ -50,6 +50,10 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.compatui.api.CompatUIEvent; +import com.android.wm.shell.compatui.api.CompatUIHandler; +import com.android.wm.shell.compatui.api.CompatUIInfo; +import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonClicked; import com.android.wm.shell.sysui.KeyguardChangeListener; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; @@ -64,6 +68,7 @@ import java.util.List; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.IntPredicate; import java.util.function.Predicate; /** @@ -71,17 +76,7 @@ import java.util.function.Predicate; * activities are in compatibility mode. */ public class CompatUIController implements OnDisplaysChangedListener, - DisplayImeController.ImePositionProcessor, KeyguardChangeListener { - - /** Callback for compat UI interaction. */ - public interface CompatUICallback { - /** 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); - /** Called when the camera compat control state is updated. */ - void onCameraControlStateUpdated(int taskId, @CameraCompatControlState int state); - } + DisplayImeController.ImePositionProcessor, KeyguardChangeListener, CompatUIHandler { private static final String TAG = "CompatUIController"; @@ -170,7 +165,7 @@ public class CompatUIController implements OnDisplaysChangedListener, private final Function<Integer, Integer> mDisappearTimeSupplier; @Nullable - private CompatUICallback mCompatUICallback; + private Consumer<CompatUIEvent> mCallback; // Indicates if the keyguard is currently showing, in which case compat UIs shouldn't // be shown. @@ -193,6 +188,12 @@ public class CompatUIController implements OnDisplaysChangedListener, */ private boolean mIsFirstReachabilityEducationRunning; + @NonNull + private final CompatUIStatusManager mCompatUIStatusManager; + + @NonNull + private final IntPredicate mInDesktopModePredicate; + public CompatUIController(@NonNull Context context, @NonNull ShellInit shellInit, @NonNull ShellController shellController, @@ -205,7 +206,9 @@ public class CompatUIController implements OnDisplaysChangedListener, @NonNull DockStateReader dockStateReader, @NonNull CompatUIConfiguration compatUIConfiguration, @NonNull CompatUIShellCommandHandler compatUIShellCommandHandler, - @NonNull AccessibilityManager accessibilityManager) { + @NonNull AccessibilityManager accessibilityManager, + @NonNull CompatUIStatusManager compatUIStatusManager, + @NonNull IntPredicate isDesktopModeEnablePredicate) { mContext = context; mShellController = shellController; mDisplayController = displayController; @@ -220,6 +223,8 @@ public class CompatUIController implements OnDisplaysChangedListener, mCompatUIShellCommandHandler = compatUIShellCommandHandler; mDisappearTimeSupplier = flags -> accessibilityManager.getRecommendedTimeoutMillis( DISAPPEAR_DELAY_MS, flags); + mCompatUIStatusManager = compatUIStatusManager; + mInDesktopModePredicate = isDesktopModeEnablePredicate; shellInit.addInitCallback(this::onInit, this); } @@ -230,21 +235,22 @@ public class CompatUIController implements OnDisplaysChangedListener, mCompatUIShellCommandHandler.onInit(); } - /** Sets the callback for Compat UI interactions. */ - public void setCompatUICallback(@NonNull CompatUICallback compatUiCallback) { - mCompatUICallback = compatUiCallback; + /** Sets the callback for UI interactions. */ + @Override + public void setCallback(@Nullable Consumer<CompatUIEvent> callback) { + mCallback = callback; } /** * Called when the Task info changed. Creates and updates the compat UI if there is an * activity in size compat, or removes the UI if there is no size compat activity. * - * @param taskInfo {@link TaskInfo} task the activity is in. - * @param taskListener listener to handle the Task Surface placement. + * @param compatUIInfo {@link CompatUIInfo} encapsulates information about the task and listener */ - public void onCompatInfoChanged(@NonNull TaskInfo taskInfo, - @Nullable ShellTaskOrganizer.TaskListener taskListener) { - if (taskInfo != null && !taskInfo.appCompatTaskInfo.topActivityInSizeCompat) { + public void onCompatInfoChanged(@NonNull CompatUIInfo compatUIInfo) { + final TaskInfo taskInfo = compatUIInfo.getTaskInfo(); + final ShellTaskOrganizer.TaskListener taskListener = compatUIInfo.getListener(); + if (taskInfo != null && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()) { mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId); } @@ -252,7 +258,9 @@ public class CompatUIController implements OnDisplaysChangedListener, updateActiveTaskInfo(taskInfo); } - if (taskInfo.configuration == null || taskListener == null) { + // We close all the Compat UI educations in case we're in desktop mode. + if (taskInfo.configuration == null || taskListener == null + || isInDesktopMode(taskInfo.displayId)) { // Null token means the current foreground activity is not in compatibility mode. removeLayouts(taskInfo.taskId); return; @@ -262,16 +270,16 @@ public class CompatUIController implements OnDisplaysChangedListener, // basically cancel all the onboarding flow. We don't have to ignore events in case // the app is in size compat mode. if (mIsFirstReachabilityEducationRunning) { - if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap - && !taskInfo.appCompatTaskInfo.topActivityInSizeCompat) { + if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap() + && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()) { return; } mIsFirstReachabilityEducationRunning = false; } - if (taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed) { - if (taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled) { + if (taskInfo.appCompatTaskInfo.isTopActivityLetterboxed()) { + if (taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled()) { createOrUpdateLetterboxEduLayout(taskInfo, taskListener); - } else if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap) { + } else if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap()) { // In this case the app is letterboxed and the letterbox education // is disabled. In this case we need to understand if it's the first // time we show the reachability education. When this is happening @@ -288,7 +296,7 @@ public class CompatUIController implements OnDisplaysChangedListener, // We activate the first reachability education if the double-tap is enabled. // If the double tap is not enabled (e.g. thin letterbox) we just set the value // of the education being seen. - if (taskInfo.appCompatTaskInfo.isLetterboxDoubleTapEnabled) { + if (taskInfo.appCompatTaskInfo.isLetterboxDoubleTapEnabled()) { mIsFirstReachabilityEducationRunning = true; createOrUpdateReachabilityEduLayout(taskInfo, taskListener); return; @@ -299,7 +307,7 @@ public class CompatUIController implements OnDisplaysChangedListener, createOrUpdateCompatLayout(taskInfo, taskListener); createOrUpdateRestartDialogLayout(taskInfo, taskListener); if (mCompatUIConfiguration.getHasSeenLetterboxEducation(taskInfo.userId)) { - if (taskInfo.appCompatTaskInfo.isLetterboxDoubleTapEnabled) { + if (taskInfo.appCompatTaskInfo.isLetterboxDoubleTapEnabled()) { createOrUpdateReachabilityEduLayout(taskInfo, taskListener); } // The user aspect ratio button should not be handled when a new TaskInfo is @@ -311,7 +319,7 @@ public class CompatUIController implements OnDisplaysChangedListener, } return; } - if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap) { + if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap()) { createOrUpdateUserAspectRatioSettingsLayout(taskInfo, taskListener); } } @@ -351,7 +359,6 @@ public class CompatUIController implements OnDisplaysChangedListener, mOnInsetsChangedListeners.remove(displayId); } - @Override public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { updateDisplayLayout(displayId); @@ -466,7 +473,7 @@ public class CompatUIController implements OnDisplaysChangedListener, CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { return new CompatUIWindowManager(context, - taskInfo, mSyncQueue, mCompatUICallback, taskListener, + taskInfo, mSyncQueue, mCallback, taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), mCompatUIHintsState, mCompatUIConfiguration, this::onRestartButtonClicked); } @@ -478,9 +485,9 @@ public class CompatUIController implements OnDisplaysChangedListener, taskInfoState.first)) { // We need to show the dialog mSetOfTaskIdsShowingRestartDialog.add(taskInfoState.first.taskId); - onCompatInfoChanged(taskInfoState.first, taskInfoState.second); + onCompatInfoChanged(new CompatUIInfo(taskInfoState.first, taskInfoState.second)); } else { - mCompatUICallback.onSizeCompatRestartButtonClicked(taskInfoState.first.taskId); + mCallback.accept(new SizeCompatRestartButtonClicked(taskInfoState.first.taskId)); } } @@ -526,7 +533,7 @@ public class CompatUIController implements OnDisplaysChangedListener, mSyncQueue, taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), mTransitionsLazy.get(), stateInfo -> createOrUpdateReachabilityEduLayout(stateInfo.first, stateInfo.second), - mDockStateReader, mCompatUIConfiguration); + mDockStateReader, mCompatUIConfiguration, mCompatUIStatusManager); } private void createOrUpdateRestartDialogLayout(@NonNull TaskInfo taskInfo, @@ -575,13 +582,13 @@ public class CompatUIController implements OnDisplaysChangedListener, private void onRestartDialogCallback( Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) { mTaskIdToRestartDialogWindowManagerMap.remove(stateInfo.first.taskId); - mCompatUICallback.onSizeCompatRestartButtonClicked(stateInfo.first.taskId); + mCallback.accept(new SizeCompatRestartButtonClicked(stateInfo.first.taskId)); } private void onRestartDialogDismissCallback( Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) { mSetOfTaskIdsShowingRestartDialog.remove(stateInfo.first.taskId); - onCompatInfoChanged(stateInfo.first, stateInfo.second); + onCompatInfoChanged(new CompatUIInfo(stateInfo.first, stateInfo.second)); } private void createOrUpdateReachabilityEduLayout(@NonNull TaskInfo taskInfo, @@ -693,7 +700,8 @@ public class CompatUIController implements OnDisplaysChangedListener, mContext.startActivityAsUser(intent, userHandle); } - private void removeLayouts(int taskId) { + @VisibleForTesting + void removeLayouts(int taskId) { final CompatUIWindowManager compatLayout = mActiveCompatLayouts.get(taskId); if (compatLayout != null) { compatLayout.release(); @@ -823,7 +831,11 @@ public class CompatUIController implements OnDisplaysChangedListener, */ static class CompatUIHintsState { boolean mHasShownSizeCompatHint; - boolean mHasShownCameraCompatHint; boolean mHasShownUserAspectRatioSettingsButtonHint; } + + private boolean isInDesktopMode(int displayId) { + return Flags.skipCompatUiEducationInDesktopMode() + && mInDesktopModePredicate.test(displayId); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java index 2b0bd3272ed2..688f8ca2dc75 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java @@ -16,10 +16,7 @@ package com.android.wm.shell.compatui; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; - import android.annotation.IdRes; -import android.app.CameraCompatTaskInfo.CameraCompatControlState; import android.content.Context; import android.util.AttributeSet; import android.view.View; @@ -57,28 +54,10 @@ class CompatUILayout extends LinearLayout { mWindowManager = windowManager; } - void updateCameraTreatmentButton(@CameraCompatControlState int newState) { - int buttonBkgId = newState == CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED - ? R.drawable.camera_compat_treatment_suggested_ripple - : R.drawable.camera_compat_treatment_applied_ripple; - int hintStringId = newState == CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED - ? R.string.camera_compat_treatment_suggested_button_description - : R.string.camera_compat_treatment_applied_button_description; - final ImageButton button = findViewById(R.id.camera_compat_treatment_button); - button.setImageResource(buttonBkgId); - button.setContentDescription(getResources().getString(hintStringId)); - final LinearLayout hint = findViewById(R.id.camera_compat_hint); - ((TextView) hint.findViewById(R.id.compat_mode_hint_text)).setText(hintStringId); - } - void setSizeCompatHintVisibility(boolean show) { setViewVisibility(R.id.size_compat_hint, show); } - void setCameraCompatHintVisibility(boolean show) { - setViewVisibility(R.id.camera_compat_hint, show); - } - void setRestartButtonVisibility(boolean show) { setViewVisibility(R.id.size_compat_restart_button, show); // Hint should never be visible without button. @@ -87,14 +66,6 @@ class CompatUILayout extends LinearLayout { } } - void setCameraControlVisibility(boolean show) { - setViewVisibility(R.id.camera_compat_control, show); - // Hint should never be visible without button. - if (!show) { - setCameraCompatHintVisibility(/* show= */ false); - } - } - private void setViewVisibility(@IdRes int resId, boolean show) { final View view = findViewById(resId); int visibility = show ? View.VISIBLE : View.GONE; @@ -127,26 +98,5 @@ class CompatUILayout extends LinearLayout { ((TextView) sizeCompatHint.findViewById(R.id.compat_mode_hint_text)) .setText(R.string.restart_button_description); sizeCompatHint.setOnClickListener(view -> setSizeCompatHintVisibility(/* show= */ false)); - - final ImageButton cameraTreatmentButton = - findViewById(R.id.camera_compat_treatment_button); - cameraTreatmentButton.setOnClickListener( - view -> mWindowManager.onCameraTreatmentButtonClicked()); - cameraTreatmentButton.setOnLongClickListener(view -> { - mWindowManager.onCameraButtonLongClicked(); - return true; - }); - - final ImageButton cameraDismissButton = findViewById(R.id.camera_compat_dismiss_button); - cameraDismissButton.setOnClickListener( - view -> mWindowManager.onCameraDismissButtonClicked()); - cameraDismissButton.setOnLongClickListener(view -> { - mWindowManager.onCameraButtonLongClicked(); - return true; - }); - - final LinearLayout cameraCompatHint = findViewById(R.id.camera_compat_hint); - cameraCompatHint.setOnClickListener( - view -> setCameraCompatHintVisibility(/* show= */ false)); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIStatusManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIStatusManager.java new file mode 100644 index 000000000000..915a8a149d54 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIStatusManager.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 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.compatui; + +import android.annotation.NonNull; + +import java.util.function.IntConsumer; +import java.util.function.IntSupplier; + +/** Handle the visibility state of the Compat UI components. */ +public class CompatUIStatusManager { + + public static final int COMPAT_UI_EDUCATION_HIDDEN = 0; + public static final int COMPAT_UI_EDUCATION_VISIBLE = 1; + + @NonNull + private final IntConsumer mWriter; + @NonNull + private final IntSupplier mReader; + + public CompatUIStatusManager(@NonNull IntConsumer writer, @NonNull IntSupplier reader) { + mWriter = writer; + mReader = reader; + } + + public CompatUIStatusManager() { + this(i -> { }, () -> COMPAT_UI_EDUCATION_HIDDEN); + } + + void onEducationShown() { + mWriter.accept(COMPAT_UI_EDUCATION_VISIBLE); + } + + void onEducationHidden() { + mWriter.accept(COMPAT_UI_EDUCATION_HIDDEN); + } + + boolean isEducationVisible() { + return mReader.getAsInt() == COMPAT_UI_EDUCATION_VISIBLE; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java index 3ab1fad2b203..4d15605c756a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java @@ -16,23 +16,18 @@ package com.android.wm.shell.compatui; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP; import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.CameraCompatTaskInfo.CameraCompatControlState; import android.app.TaskInfo; import android.content.Context; import android.graphics.Rect; -import android.util.Log; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; +import android.window.flags.DesktopModeFlags; import com.android.internal.annotations.VisibleForTesting; import com.android.window.flags.Flags; @@ -40,8 +35,10 @@ import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.compatui.CompatUIController.CompatUICallback; import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; +import com.android.wm.shell.compatui.api.CompatUIEvent; +import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonAppeared; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import java.util.function.Consumer; @@ -50,10 +47,13 @@ import java.util.function.Consumer; */ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { - private final CompatUICallback mCallback; + @NonNull + private final Consumer<CompatUIEvent> mCallback; + @NonNull private final CompatUIConfiguration mCompatUIConfiguration; + @NonNull private final Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked; // Remember the last reported states in case visibility changes due to keyguard or IME updates. @@ -61,10 +61,7 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { boolean mHasSizeCompat; @VisibleForTesting - @CameraCompatControlState - int mCameraCompatControlState = CAMERA_COMPAT_CONTROL_HIDDEN; - - @VisibleForTesting + @NonNull CompatUIHintsState mCompatUIHintsState; @Nullable @@ -73,20 +70,23 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { private final float mHideScmTolerance; - CompatUIWindowManager(Context context, TaskInfo taskInfo, - SyncTransactionQueue syncQueue, CompatUICallback callback, - ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, - CompatUIHintsState compatUIHintsState, CompatUIConfiguration compatUIConfiguration, - Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartButtonClicked) { + CompatUIWindowManager(@NonNull Context context, @NonNull TaskInfo taskInfo, + @NonNull SyncTransactionQueue syncQueue, + @NonNull Consumer<CompatUIEvent> callback, + @Nullable ShellTaskOrganizer.TaskListener taskListener, + @Nullable DisplayLayout displayLayout, + @NonNull CompatUIHintsState compatUIHintsState, + @NonNull CompatUIConfiguration compatUIConfiguration, + @NonNull Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> + onRestartButtonClicked) { super(context, taskInfo, syncQueue, taskListener, displayLayout); mCallback = callback; - mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat; - if (Flags.enableDesktopWindowingMode() && Flags.enableWindowingDynamicInitialBounds()) { + mHasSizeCompat = taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat(); + if (DesktopModeStatus.canEnterDesktopMode(context) + && DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isTrue()) { // Don't show the SCM button for freeform tasks mHasSizeCompat &= !taskInfo.isFreeform(); } - mCameraCompatControlState = - taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState; mCompatUIHintsState = compatUIHintsState; mCompatUIConfiguration = compatUIConfiguration; mOnRestartButtonClicked = onRestartButtonClicked; @@ -110,8 +110,7 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { @Override protected boolean eligibleToShowLayout() { - return (mHasSizeCompat && shouldShowSizeCompatRestartButton(getLastTaskInfo())) - || shouldShowCameraControl(); + return mHasSizeCompat && shouldShowSizeCompatRestartButton(getLastTaskInfo()); } @Override @@ -122,7 +121,7 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { updateVisibilityOfViews(); if (mHasSizeCompat) { - mCallback.onSizeCompatRestartButtonAppeared(mTaskId); + mCallback.accept(new SizeCompatRestartButtonAppeared(mTaskId)); } return mLayout; @@ -138,21 +137,18 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener, boolean canShow) { final boolean prevHasSizeCompat = mHasSizeCompat; - final int prevCameraCompatControlState = mCameraCompatControlState; - mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat; - if (Flags.enableDesktopWindowingMode() && Flags.enableWindowingDynamicInitialBounds()) { + mHasSizeCompat = taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat(); + if (DesktopModeStatus.canEnterDesktopMode(mContext) + && DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isTrue()) { // Don't show the SCM button for freeform tasks mHasSizeCompat &= !taskInfo.isFreeform(); } - mCameraCompatControlState = - taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState; if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) { return false; } - if (prevHasSizeCompat != mHasSizeCompat - || prevCameraCompatControlState != mCameraCompatControlState) { + if (prevHasSizeCompat != mHasSizeCompat) { updateVisibilityOfViews(); } @@ -164,34 +160,6 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { mOnRestartButtonClicked.accept(Pair.create(getLastTaskInfo(), getTaskListener())); } - /** Called when the camera treatment button is clicked. */ - void onCameraTreatmentButtonClicked() { - if (!shouldShowCameraControl()) { - Log.w(getTag(), "Camera compat shouldn't receive clicks in the hidden state."); - return; - } - // When a camera control is shown, only two states are allowed: "treament applied" and - // "treatment suggested". Clicks on the conrol's treatment button toggle between these - // two states. - mCameraCompatControlState = - mCameraCompatControlState == CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED - ? CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED - : CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; - mCallback.onCameraControlStateUpdated(mTaskId, mCameraCompatControlState); - mLayout.updateCameraTreatmentButton(mCameraCompatControlState); - } - - /** Called when the camera dismiss button is clicked. */ - void onCameraDismissButtonClicked() { - if (!shouldShowCameraControl()) { - Log.w(getTag(), "Camera compat shouldn't receive clicks in the hidden state."); - return; - } - mCameraCompatControlState = CAMERA_COMPAT_CONTROL_DISMISSED; - mCallback.onCameraControlStateUpdated(mTaskId, CAMERA_COMPAT_CONTROL_DISMISSED); - mLayout.setCameraControlVisibility(/* show= */ false); - } - /** Called when the restart button is long clicked. */ void onRestartButtonLongClicked() { if (mLayout == null) { @@ -200,14 +168,6 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { mLayout.setSizeCompatHintVisibility(/* show= */ true); } - /** Called when either dismiss or treatment camera buttons is long clicked. */ - void onCameraButtonLongClicked() { - if (mLayout == null) { - return; - } - mLayout.setCameraCompatHintVisibility(/* show= */ true); - } - @Override @VisibleForTesting public void updateSurfacePosition() { @@ -255,6 +215,7 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { return false; } final float percentageAreaOfLetterboxInTask = (float) letterboxArea / taskArea * 100; + return percentageAreaOfLetterboxInTask < mHideScmTolerance; } @@ -269,21 +230,5 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { mLayout.setSizeCompatHintVisibility(/* show= */ true); mCompatUIHintsState.mHasShownSizeCompatHint = true; } - - // Camera control for stretched issues. - mLayout.setCameraControlVisibility(shouldShowCameraControl()); - // Only show by default for the first time. - if (shouldShowCameraControl() && !mCompatUIHintsState.mHasShownCameraCompatHint) { - mLayout.setCameraCompatHintVisibility(/* show= */ true); - mCompatUIHintsState.mHasShownCameraCompatHint = true; - } - if (shouldShowCameraControl()) { - mLayout.updateCameraTreatmentButton(mCameraCompatControlState); - } - } - - private boolean shouldShowCameraControl() { - return mCameraCompatControlState != CAMERA_COMPAT_CONTROL_HIDDEN - && mCameraCompatControlState != CAMERA_COMPAT_CONTROL_DISMISSED; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java index 0564c95aef5c..d2b4f1ab6b0d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java @@ -38,7 +38,6 @@ import android.util.Log; import android.view.IWindow; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; -import android.view.SurfaceSession; import android.view.View; import android.view.WindowManager; import android.view.WindowlessWindowManager; @@ -173,7 +172,7 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana @Override protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) { String className = getClass().getSimpleName(); - final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + final SurfaceControl.Builder builder = new SurfaceControl.Builder() .setContainerLayer() .setName(className + "Leash") .setHidden(false) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java index 623feada0172..3124a397162f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java @@ -19,6 +19,7 @@ package com.android.wm.shell.compatui; import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING; import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.TaskInfo; import android.content.Context; @@ -76,15 +77,19 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { private final DockStateReader mDockStateReader; + @NonNull + private final CompatUIStatusManager mCompatUIStatusManager; + LetterboxEduWindowManager(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, Transitions transitions, Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onDismissCallback, - DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration) { + DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration, + @NonNull CompatUIStatusManager compatUIStatusManager) { this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions, onDismissCallback, new DialogAnimationController<>(context, /* tag */ "LetterboxEduWindowManager"), - dockStateReader, compatUIConfiguration); + dockStateReader, compatUIConfiguration, compatUIStatusManager); } @VisibleForTesting @@ -93,7 +98,8 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { DisplayLayout displayLayout, Transitions transitions, Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onDismissCallback, DialogAnimationController<LetterboxEduDialogLayout> animationController, - DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration) { + DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration, + @NonNull CompatUIStatusManager compatUIStatusManager) { super(context, taskInfo, syncQueue, taskListener, displayLayout); mTransitions = transitions; mOnDismissCallback = onDismissCallback; @@ -103,8 +109,9 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { R.dimen.letterbox_education_dialog_margin); mDockStateReader = dockStateReader; mCompatUIConfiguration = compatUIConfiguration; + mCompatUIStatusManager = compatUIStatusManager; mEligibleForLetterboxEducation = - taskInfo.appCompatTaskInfo.topActivityEligibleForLetterboxEducation; + taskInfo.appCompatTaskInfo.eligibleForLetterboxEducation(); } @Override @@ -139,7 +146,7 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { protected View createLayout() { mLayout = inflateLayout(); updateDialogMargins(); - + mCompatUIStatusManager.onEducationShown(); // startEnterAnimation will be called immediately if shell-transitions are disabled. mTransitions.runOnIdle(this::startEnterAnimation); return mLayout; @@ -199,14 +206,14 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { @Override public void release() { mAnimationController.cancelAnimation(); + mCompatUIStatusManager.onEducationHidden(); super.release(); } @Override public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener, boolean canShow) { - mEligibleForLetterboxEducation = - taskInfo.appCompatTaskInfo.topActivityEligibleForLetterboxEducation; + mEligibleForLetterboxEducation = taskInfo.appCompatTaskInfo.eligibleForLetterboxEducation(); return super.updateCompatInfo(taskInfo, taskListener, canShow); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java index 07082a558744..06f2dd1a3b17 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java @@ -91,7 +91,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { Function<Integer, Integer> disappearTimeSupplier) { super(context, taskInfo, syncQueue, taskListener, displayLayout); final AppCompatTaskInfo appCompatTaskInfo = taskInfo.appCompatTaskInfo; - mIsLetterboxDoubleTapEnabled = appCompatTaskInfo.isLetterboxDoubleTapEnabled; + mIsLetterboxDoubleTapEnabled = appCompatTaskInfo.isLetterboxDoubleTapEnabled(); mLetterboxVerticalPosition = appCompatTaskInfo.topActivityLetterboxVerticalPosition; mLetterboxHorizontalPosition = appCompatTaskInfo.topActivityLetterboxHorizontalPosition; mTopActivityLetterboxWidth = appCompatTaskInfo.topActivityLetterboxWidth; @@ -148,12 +148,12 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { final int prevTopActivityLetterboxWidth = mTopActivityLetterboxWidth; final int prevTopActivityLetterboxHeight = mTopActivityLetterboxHeight; final AppCompatTaskInfo appCompatTaskInfo = taskInfo.appCompatTaskInfo; - mIsLetterboxDoubleTapEnabled = appCompatTaskInfo.isLetterboxDoubleTapEnabled; + mIsLetterboxDoubleTapEnabled = appCompatTaskInfo.isLetterboxDoubleTapEnabled(); mLetterboxVerticalPosition = appCompatTaskInfo.topActivityLetterboxVerticalPosition; mLetterboxHorizontalPosition = appCompatTaskInfo.topActivityLetterboxHorizontalPosition; mTopActivityLetterboxWidth = appCompatTaskInfo.topActivityLetterboxWidth; mTopActivityLetterboxHeight = appCompatTaskInfo.topActivityLetterboxHeight; - mHasUserDoubleTapped = appCompatTaskInfo.isFromLetterboxDoubleTap; + mHasUserDoubleTapped = appCompatTaskInfo.isFromLetterboxDoubleTap(); if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) { return false; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java index 8fb4bdbea933..3f67172ca636 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java @@ -238,14 +238,14 @@ class UserAspectRatioSettingsWindowManager extends CompatUIWindowManagerAbstract // App is not visibly letterboxed if it covers status bar/bottom insets or matches the // stable bounds, so don't show the button if (stableBounds.height() <= letterboxHeight && stableBounds.width() <= letterboxWidth - && !taskInfo.isUserFullscreenOverrideEnabled) { + && !taskInfo.isUserFullscreenOverrideEnabled()) { return false; } - return taskInfo.topActivityEligibleForUserAspectRatioButton - && (taskInfo.topActivityBoundsLetterboxed - || taskInfo.isUserFullscreenOverrideEnabled) - && !taskInfo.isSystemFullscreenOverrideEnabled + return taskInfo.eligibleForUserAspectRatioButton() + && (taskInfo.isTopActivityLetterboxed() + || taskInfo.isUserFullscreenOverrideEnabled()) + && !taskInfo.isSystemFullscreenOverrideEnabled() && Intent.ACTION_MAIN.equals(intent.getAction()) && intent.hasCategory(Intent.CATEGORY_LAUNCHER) && (!mUserAspectRatioButtonShownChecker.get() || isShowingButton()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt new file mode 100644 index 000000000000..abc26cfb3e13 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2024 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.compatui.api + +import android.content.Context +import android.content.res.Configuration +import android.graphics.PixelFormat +import android.graphics.Point +import android.os.Binder +import android.view.IWindow +import android.view.SurfaceControl +import android.view.SurfaceControlViewHost +import android.view.View +import android.view.WindowManager +import android.view.WindowlessWindowManager +import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.SyncTransactionQueue + +/** + * The component created after a {@link CompatUISpec} definition + */ +class CompatUIComponent( + private val spec: CompatUISpec, + private val id: String, + private var context: Context, + private val state: CompatUIState, + private var compatUIInfo: CompatUIInfo, + private val syncQueue: SyncTransactionQueue, + private var displayLayout: DisplayLayout? +) : WindowlessWindowManager( + compatUIInfo.taskInfo.configuration, + /* rootSurface */ + null, + /* hostInputToken */ + null +) { + + private val tag + get() = "CompatUI {id = $id}" + + private var leash: SurfaceControl? = null + + private var layout: View? = null + + /** + * Utility class for adding and releasing a View hierarchy for this [ ] to `mLeash`. + */ + protected var viewHost: SurfaceControlViewHost? = null + + override fun setConfiguration(configuration: Configuration?) { + super.setConfiguration(configuration) + configuration?.let { + context = context.createConfigurationContext(it) + } + } + + /** + * Invoked every time a new CompatUIInfo comes from core + * @param newInfo The new CompatUIInfo object + */ + fun update(newInfo: CompatUIInfo) { + updateComponentState(newInfo, state.stateForComponent(id)) + updateUI(state) + } + + fun release() { + spec.log("$tag releasing.....") + // Implementation empty + // Hiding before releasing to avoid flickering when transitioning to the Home screen. + layout?.visibility = View.GONE + layout = null + spec.layout.viewReleaser() + spec.log("$tag layout releaser invoked!") + viewHost?.release() + viewHost = null + leash?.run { + val localLeash: SurfaceControl = this + syncQueue.runInSync { t: SurfaceControl.Transaction -> + t.remove( + localLeash + ) + } + leash = null + spec.log("$tag leash removed") + } + spec.log("$tag released") + } + + override fun getParentSurface( + window: IWindow, + attrs: WindowManager.LayoutParams + ): SurfaceControl? { + val className = javaClass.simpleName + val builder = SurfaceControl.Builder() + .setContainerLayer() + .setName(className + "Leash") + .setHidden(false) + .setCallsite("$className#attachToParentSurface") + attachToParentSurface(builder) + leash = builder.build() + initSurface(leash) + return leash + } + + fun attachToParentSurface(builder: SurfaceControl.Builder) { + compatUIInfo.listener?.attachChildSurfaceToTask(compatUIInfo.taskInfo.taskId, builder) + } + + fun initLayout(newCompatUIInfo: CompatUIInfo) { + compatUIInfo = newCompatUIInfo + spec.log("$tag updating...") + check(viewHost == null) { "A UI has already been created with this window manager." } + val componentState: CompatUIComponentState? = state.stateForComponent(id) + spec.log("$tag state: $componentState") + // We inflate the layout + layout = spec.layout.viewBuilder(context, compatUIInfo, componentState) + spec.log("$tag layout: $layout") + viewHost = createSurfaceViewHost().apply { + spec.log("$tag adding view $layout to host $this") + setView(layout!!, getWindowLayoutParams()) + } + updateSurfacePosition() + } + + /** Creates a [SurfaceControlViewHost] for this window manager. */ + fun createSurfaceViewHost(): SurfaceControlViewHost = + SurfaceControlViewHost(context, context.display, this, javaClass.simpleName) + + fun relayout() { + spec.log("$tag relayout...") + viewHost?.run { + relayout(getWindowLayoutParams()) + updateSurfacePosition() + } + } + + protected fun updateSurfacePosition() { + spec.log("$tag updateSurfacePosition on layout $layout") + layout?.let { + updateSurfacePosition( + spec.layout.positionFactory( + it, + compatUIInfo, + state.sharedState, + state.stateForComponent(id) + ) + ) + } + } + + protected fun getWindowLayoutParams(width: Int, height: Int): WindowManager.LayoutParams { + // Cannot be wrap_content as this determines the actual window size + val winParams = + WindowManager.LayoutParams( + width, + height, + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + spec.layout.layoutParamFlags, + PixelFormat.TRANSLUCENT + ) + winParams.token = Binder() + winParams.title = javaClass.simpleName + compatUIInfo.taskInfo.taskId + winParams.privateFlags = + winParams.privateFlags or (WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION + or WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) + spec.log("$tag getWindowLayoutParams $winParams") + return winParams + } + + /** Gets the layout params. */ + protected fun getWindowLayoutParams(): WindowManager.LayoutParams = + layout?.run { + measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + spec.log( + "$tag getWindowLayoutParams size: ${measuredWidth}x$measuredHeight" + ) + return getWindowLayoutParams(measuredWidth, measuredHeight) + } ?: WindowManager.LayoutParams() + + protected fun updateSurfacePosition(position: Point) { + spec.log("$tag updateSurfacePosition on leash $leash") + leash?.run { + syncQueue.runInSync { t: SurfaceControl.Transaction -> + if (!isValid) { + spec.log("$tag The leash has been released.") + return@runInSync + } + spec.log("$tag settings position $position") + t.setPosition(this, position.x.toFloat(), position.y.toFloat()) + } + } + } + + private fun updateComponentState( + newInfo: CompatUIInfo, + componentState: CompatUIComponentState? + ) { + spec.log("$tag component state updating.... $componentState") + compatUIInfo = newInfo + } + + private fun updateUI(state: CompatUIState) { + spec.log("$tag updating ui") + setConfiguration(compatUIInfo.taskInfo.configuration) + val componentState: CompatUIComponentState? = state.stateForComponent(id) + layout?.run { + spec.log("$tag viewBinder execution...") + spec.layout.viewBinder(this, compatUIInfo, state.sharedState, componentState) + relayout() + } + } + + private fun initSurface(leash: SurfaceControl?) { + syncQueue.runInSync { t: SurfaceControl.Transaction -> + if (leash == null || !leash.isValid) { + spec.log("$tag The leash has been released.") + return@runInSync + } + t.setLayer(leash, spec.layout.zOrder) + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponentFactory.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponentFactory.kt new file mode 100644 index 000000000000..55821ffdb6bd --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponentFactory.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 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.compatui.api + +/** + * Abstracts the component responsible for the creation of a component + */ +interface CompatUIComponentFactory { + + fun create( + spec: CompatUISpec, + compId: String, + state: CompatUIState, + compatUIInfo: CompatUIInfo, + ): CompatUIComponent +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponentIdGenerator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponentIdGenerator.kt new file mode 100644 index 000000000000..7d663fa809f3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponentIdGenerator.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 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.compatui.api + +/** + * Any object responsible to generate an id for a component. + */ +interface CompatUIComponentIdGenerator { + + /** + * Generates the unique id for a component given a {@link CompatUIInfo} and component + * {@link CompatUISpec}. + * @param compatUIInfo The object encapsulating information about the current Task. + * @param spec The {@link CompatUISpec} for the component. + */ + fun generateId(compatUIInfo: CompatUIInfo, spec: CompatUISpec): String +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponentState.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponentState.kt new file mode 100644 index 000000000000..ec21924fbe16 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponentState.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 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.compatui.api + +/** + * Abstraction of all the component specific state. Each + * component can create its own state implementing this interface. + */ +interface CompatUIComponentState diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIEvent.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIEvent.kt new file mode 100644 index 000000000000..4a0cf9843722 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIEvent.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 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.compatui.api + +/** + * Abstraction for all the possible Compat UI Component events. + */ +interface CompatUIEvent { + /** + * Unique event identifier + */ + val eventId: Int + + @Suppress("UNCHECKED_CAST") + fun <T : CompatUIEvent> asType(): T? = this as? T + + fun <T : CompatUIEvent> asType(clazz: Class<T>): T? { + return if (clazz.isInstance(this)) clazz.cast(this) else null + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt new file mode 100644 index 000000000000..817e554b550e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 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.compatui.api + +import java.util.function.Consumer + +/** + * Abstraction for the objects responsible to handle all the CompatUI components and the + * communication with the server. + */ +interface CompatUIHandler { + /** + * Invoked when a new model is coming from the server. + */ + fun onCompatInfoChanged(compatUIInfo: CompatUIInfo) + + /** + * Optional reference to the object responsible to send {@link CompatUIEvent} + */ + fun setCallback(compatUIEventSender: Consumer<CompatUIEvent>?) +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIInfo.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIInfo.kt new file mode 100644 index 000000000000..dbbf049792f5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIInfo.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 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.compatui.api + +import android.app.TaskInfo +import com.android.wm.shell.ShellTaskOrganizer + +/** + * Encapsulate the info of the message from core. + */ +data class CompatUIInfo(val taskInfo: TaskInfo, val listener: ShellTaskOrganizer.TaskListener?)
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRepository.kt new file mode 100644 index 000000000000..cb54d89a5714 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRepository.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 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.compatui.api + +/** + * Abstraction for the repository of all the available CompatUISpec + */ +interface CompatUIRepository { + /** + * Adds a {@link CompatUISpec} to the repository + * @throws IllegalStateException in case of illegal spec + */ + fun addSpec(spec: CompatUISpec) + + /** + * Iterates on the list of available {@link CompatUISpec} invoking + * fn for each of them. + */ + fun iterateOn(fn: (CompatUISpec) -> Unit) + + /** + * Returns the {@link CompatUISpec} for a given key + */ + fun findSpec(name: String): CompatUISpec? +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISharedState.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISharedState.kt new file mode 100644 index 000000000000..33e0d468e4bc --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISharedState.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 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.compatui.api + +/** + * Represents the state shared between all the components. + */ +class CompatUISharedState
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt new file mode 100644 index 000000000000..de400f49d64b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 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.compatui.api + +import android.content.Context +import android.graphics.Point +import android.view.View +import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE +import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.protolog.ShellProtoLogGroup + +/** + * Defines the predicates to invoke for understanding if a component can be created or destroyed. + */ +class CompatUILifecyclePredicates( + // Predicate evaluating to true if the component needs to be created + val creationPredicate: (CompatUIInfo, CompatUISharedState) -> Boolean, + // Predicate evaluating to true if the component needs to be destroyed + val removalPredicate: ( + CompatUIInfo, + CompatUISharedState, + CompatUIComponentState? + ) -> Boolean, + // Builder for the initial state of the component + val stateBuilder: ( + CompatUIInfo, + CompatUISharedState + ) -> CompatUIComponentState? = { _, _ -> null } +) + +/** + * Layout configuration + */ +data class CompatUILayout( + val zOrder: Int = 0, + val layoutParamFlags: Int = FLAG_NOT_FOCUSABLE or FLAG_NOT_TOUCH_MODAL, + val viewBuilder: (Context, CompatUIInfo, CompatUIComponentState?) -> View, + val viewBinder: ( + View, + CompatUIInfo, + CompatUISharedState, + CompatUIComponentState? + ) -> Unit = { _, _, _, _ -> }, + val positionFactory: ( + View, + CompatUIInfo, + CompatUISharedState, + CompatUIComponentState? + ) -> Point, + val viewReleaser: () -> Unit = {} +) + +/** + * Describes each compat ui component to the framework. + */ +class CompatUISpec( + val log: (String) -> Unit = { str -> ProtoLog.v(ShellProtoLogGroup.WM_SHELL_COMPAT_UI, str) }, + // Unique name for the component. It's used for debug and for generating the + // unique component identifier in the system. + val name: String, + // The lifecycle definition + val lifecycle: CompatUILifecyclePredicates, + // The layout definition + val layout: CompatUILayout +) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIState.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIState.kt new file mode 100644 index 000000000000..68307b437efa --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIState.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 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.compatui.api + +/** + * Singleton which contains the global state of the compat ui system. + */ +class CompatUIState { + + private val components = mutableMapOf<String, CompatUIComponent>() + + val sharedState = CompatUISharedState() + + val componentStates = mutableMapOf<String, CompatUIComponentState>() + + /** + * @return The CompatUIComponent for the given componentId if it exists. + */ + fun getUIComponent(componentId: String): CompatUIComponent? = + components[componentId] + + /** + * Registers a component for a given componentId along with its optional state. + * <p/> + * @param componentId The identifier for the component to register. + * @param comp The {@link CompatUIComponent} instance to register. + * @param componentState The optional state specific of the component. Not all components + * have a specific state so it can be null. + */ + fun registerUIComponent( + componentId: String, + comp: CompatUIComponent, + componentState: CompatUIComponentState? + ) { + components[componentId] = comp + componentState?.let { + componentStates[componentId] = componentState + } + } + + /** + * Unregister a component for a given componentId. + * <p/> + * @param componentId The identifier for the component to register. + */ + fun unregisterUIComponent(componentId: String) { + components.remove(componentId) + componentStates.remove(componentId) + } + + /** + * Get access to the specific {@link CompatUIComponentState} for a {@link CompatUIComponent} + * with a given identifier. + * <p/> + * @param componentId The identifier of the {@link CompatUIComponent}. + * @return The optional state for the component of the provided id. + */ + @Suppress("UNCHECKED_CAST") + fun <T : CompatUIComponentState> stateForComponent(componentId: String) = + componentStates[componentId] as? T +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/components/RestartButtonSpec.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/components/RestartButtonSpec.kt new file mode 100644 index 000000000000..e18cc0e5d416 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/components/RestartButtonSpec.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 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.compatui.components + +import android.annotation.SuppressLint +import android.graphics.Point +import android.view.LayoutInflater +import android.view.View +import android.window.TaskConstants +import com.android.wm.shell.R +import com.android.wm.shell.compatui.api.CompatUILayout +import com.android.wm.shell.compatui.api.CompatUILifecyclePredicates +import com.android.wm.shell.compatui.api.CompatUISpec + +/** + * CompatUISpec for the Restart Button + */ +@SuppressLint("InflateParams") +val RestartButtonSpec = CompatUISpec( + name = "restartButton", + lifecycle = CompatUILifecyclePredicates( + creationPredicate = { info, _ -> + info.taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat + }, + removalPredicate = { info, _, _ -> + !info.taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat + } + ), + layout = CompatUILayout( + zOrder = TaskConstants.TASK_CHILD_LAYER_COMPAT_UI + 10, + viewBuilder = { ctx, _, _ -> + LayoutInflater.from(ctx).inflate( + R.layout.compat_ui_restart_button_layout, + null + ) + }, + viewBinder = { view, _, _, _ -> + view.visibility = View.VISIBLE + view.findViewById<View>(R.id.size_compat_restart_button)?.visibility = View.VISIBLE + }, + // TODO(b/360288344): Calculate right position from stable bounds + positionFactory = { _, _, _, _ -> Point(500, 500) } + ) +) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIEvents.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIEvents.kt new file mode 100644 index 000000000000..db3fda028ef2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIEvents.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 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.compatui.impl + +import com.android.wm.shell.compatui.api.CompatUIEvent + +internal const val SIZE_COMPAT_RESTART_BUTTON_APPEARED = 0 +internal const val SIZE_COMPAT_RESTART_BUTTON_CLICKED = 1 + +/** + * All the {@link CompatUIEvent} the Compat UI Framework can handle + */ +sealed class CompatUIEvents(override val eventId: Int) : CompatUIEvent { + /** Sent when the size compat restart button appears. */ + data class SizeCompatRestartButtonAppeared(val taskId: Int) : + CompatUIEvents(SIZE_COMPAT_RESTART_BUTTON_APPEARED) + + /** Sent when the size compat restart button is clicked. */ + data class SizeCompatRestartButtonClicked(val taskId: Int) : + CompatUIEvents(SIZE_COMPAT_RESTART_BUTTON_CLICKED) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIComponentFactory.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIComponentFactory.kt new file mode 100644 index 000000000000..4eea6a31dd27 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIComponentFactory.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 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.compatui.impl + +import android.content.Context +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.compatui.api.CompatUIComponent +import com.android.wm.shell.compatui.api.CompatUIComponentFactory +import com.android.wm.shell.compatui.api.CompatUIInfo +import com.android.wm.shell.compatui.api.CompatUISpec +import com.android.wm.shell.compatui.api.CompatUIState + +/** + * Default {@link CompatUIComponentFactory } implementation + */ +class DefaultCompatUIComponentFactory( + private val context: Context, + private val syncQueue: SyncTransactionQueue, + private val displayController: DisplayController +) : CompatUIComponentFactory { + override fun create( + spec: CompatUISpec, + compId: String, + state: CompatUIState, + compatUIInfo: CompatUIInfo + ): CompatUIComponent = + CompatUIComponent( + spec, + compId, + context, + state, + compatUIInfo, + syncQueue, + displayController.getDisplayLayout(compatUIInfo.taskInfo.displayId) + ) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt new file mode 100644 index 000000000000..02db85a4f99d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2024 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.compatui.impl + +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.compatui.api.CompatUIComponentFactory +import com.android.wm.shell.compatui.api.CompatUIComponentIdGenerator +import com.android.wm.shell.compatui.api.CompatUIEvent +import com.android.wm.shell.compatui.api.CompatUIHandler +import com.android.wm.shell.compatui.api.CompatUIInfo +import com.android.wm.shell.compatui.api.CompatUIRepository +import com.android.wm.shell.compatui.api.CompatUIState +import java.util.function.Consumer + +/** + * Default implementation of {@link CompatUIHandler} to handle CompatUI components + */ +class DefaultCompatUIHandler( + private val compatUIRepository: CompatUIRepository, + private val compatUIState: CompatUIState, + private val componentIdGenerator: CompatUIComponentIdGenerator, + private val componentFactory: CompatUIComponentFactory, + private val executor: ShellExecutor +) : CompatUIHandler { + + private var compatUIEventSender: Consumer<CompatUIEvent>? = null + + override fun onCompatInfoChanged(compatUIInfo: CompatUIInfo) { + compatUIRepository.iterateOn { spec -> + // We get the identifier for the component depending on the task and spec + val componentId = componentIdGenerator.generateId(compatUIInfo, spec) + spec.log("Evaluating component $componentId") + // We check in the state if the component does not yet exist + var component = compatUIState.getUIComponent(componentId) + if (component == null) { + spec.log("Component $componentId not present") + // We evaluate the predicate + if (spec.lifecycle.creationPredicate(compatUIInfo, compatUIState.sharedState)) { + spec.log("Component $componentId should be created") + // We create the component and store in the + // global state + component = + componentFactory.create(spec, componentId, compatUIState, compatUIInfo) + spec.log("Component $componentId created $component") + // We initialize the state for the component + val compState = spec.lifecycle.stateBuilder( + compatUIInfo, + compatUIState.sharedState + ) + spec.log("Component $componentId initial state $compState") + compatUIState.registerUIComponent(componentId, component, compState) + spec.log("Component $componentId registered") + // We initialize the layout for the component + component.initLayout(compatUIInfo) + spec.log("Component $componentId layout created") + // Now we can invoke the update passing the shared state and + // the state specific to the component + executor.execute { + component.update(compatUIInfo) + spec.log("Component $componentId updated with $compatUIInfo") + } + } + } else { + // The component is present. We check if we need to remove it + if (spec.lifecycle.removalPredicate( + compatUIInfo, + compatUIState.sharedState, + compatUIState.stateForComponent(componentId) + )) { + spec.log("Component $componentId should be removed") + // We clean the component + component.release() + spec.log("Component $componentId released") + compatUIState.unregisterUIComponent(componentId) + spec.log("Component $componentId removed from registry") + } else { + executor.execute { + // The component exists so we need to invoke the update methods + component.update(compatUIInfo) + spec.log("Component $componentId updated with $compatUIInfo") + } + } + } + } + // Empty at the moment + } + + override fun setCallback(compatUIEventSender: Consumer<CompatUIEvent>?) { + this.compatUIEventSender = compatUIEventSender + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepository.kt new file mode 100644 index 000000000000..10d9425c85ea --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepository.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 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.compatui.impl + +import com.android.wm.shell.compatui.api.CompatUIRepository +import com.android.wm.shell.compatui.api.CompatUISpec + +/** + * Default {@link CompatUIRepository} implementation + */ +class DefaultCompatUIRepository : CompatUIRepository { + + private val allSpecs = mutableMapOf<String, CompatUISpec>() + + override fun addSpec(spec: CompatUISpec) { + if (allSpecs[spec.name] != null) { + throw IllegalStateException("Spec with id:${spec.name} already present") + } + allSpecs[spec.name] = spec + } + + override fun iterateOn(fn: (CompatUISpec) -> Unit) = + allSpecs.values.forEach(fn) + + override fun findSpec(name: String): CompatUISpec? = + allSpecs[name] +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultComponentIdGenerator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultComponentIdGenerator.kt new file mode 100644 index 000000000000..446291b3d17b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultComponentIdGenerator.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 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.compatui.impl + +import com.android.wm.shell.compatui.api.CompatUIComponentIdGenerator +import com.android.wm.shell.compatui.api.CompatUIInfo +import com.android.wm.shell.compatui.api.CompatUISpec + +/** + * Default {@link CompatUIComponentIdGenerator} implementation. + */ +class DefaultComponentIdGenerator : CompatUIComponentIdGenerator { + /** + * Simple implementation generating the id from taskId and component name. + */ + override fun generateId(compatUIInfo: CompatUIInfo, spec: CompatUISpec): String = + "${compatUIInfo.taskInfo.taskId}-${spec.name}" +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java index 011093718671..33e4fd8c1a46 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java @@ -30,9 +30,9 @@ import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; -import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.dagger.pip.TvPipModule; import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.splitscreen.tv.TvSplitScreenController; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 609e5af5c5b0..bec2ea58e106 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -16,13 +16,18 @@ package com.android.wm.shell.dagger; +import static android.provider.Settings.Secure.COMPAT_UI_EDUCATION_SHOWING; + +import static com.android.wm.shell.compatui.CompatUIStatusManager.COMPAT_UI_EDUCATION_HIDDEN; import static com.android.wm.shell.onehanded.OneHandedController.SUPPORT_ONE_HANDED_MODE; +import android.annotation.NonNull; import android.app.ActivityTaskManager; import android.content.Context; import android.content.pm.PackageManager; import android.os.Handler; import android.os.SystemProperties; +import android.provider.Settings; import android.view.IWindowManager; import android.view.accessibility.AccessibilityManager; import android.window.SystemPerformanceHinter; @@ -57,7 +62,6 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerImpl; -import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.pip.PhonePipKeepClearAlgorithm; import com.android.wm.shell.common.pip.PhoneSizeSpecSource; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; @@ -71,6 +75,17 @@ import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.compatui.CompatUIConfiguration; import com.android.wm.shell.compatui.CompatUIController; import com.android.wm.shell.compatui.CompatUIShellCommandHandler; +import com.android.wm.shell.compatui.CompatUIStatusManager; +import com.android.wm.shell.compatui.api.CompatUIComponentFactory; +import com.android.wm.shell.compatui.api.CompatUIComponentIdGenerator; +import com.android.wm.shell.compatui.api.CompatUIHandler; +import com.android.wm.shell.compatui.api.CompatUIRepository; +import com.android.wm.shell.compatui.api.CompatUIState; +import com.android.wm.shell.compatui.components.RestartButtonSpecKt; +import com.android.wm.shell.compatui.impl.DefaultCompatUIComponentFactory; +import com.android.wm.shell.compatui.impl.DefaultCompatUIHandler; +import com.android.wm.shell.compatui.impl.DefaultCompatUIRepository; +import com.android.wm.shell.compatui.impl.DefaultComponentIdGenerator; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.desktopmode.DesktopTasksController; @@ -88,12 +103,13 @@ import com.android.wm.shell.recents.RecentTasks; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.recents.RecentsTransitionHandler; import com.android.wm.shell.recents.TaskStackTransitionObserver; -import com.android.wm.shell.shared.DesktopModeStatus; import com.android.wm.shell.shared.ShellTransitions; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.annotations.ShellAnimationThread; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.annotations.ShellSplashscreenThread; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.startingsurface.StartingSurface; @@ -121,6 +137,7 @@ import dagger.Module; import dagger.Provides; import java.util.Optional; +import java.util.function.IntPredicate; /** * Provides basic dependencies from {@link com.android.wm.shell}, these dependencies are only @@ -131,7 +148,11 @@ import java.util.Optional; * dependencies that are device/form factor SystemUI implementation specific should go into their * respective modules (ie. {@link WMShellModule} for handheld, {@link TvWMShellModule} for tv, etc.) */ -@Module(includes = WMShellConcurrencyModule.class) +@Module( + includes = { + WMShellConcurrencyModule.class, + WMShellCoroutinesModule.class + }) public abstract class WMShellBaseModule { // @@ -211,7 +232,7 @@ public abstract class WMShellBaseModule { Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, - Optional<CompatUIController> compatUI, + Optional<CompatUIHandler> compatUI, Optional<UnfoldAnimationController> unfoldAnimationController, Optional<RecentTasksController> recentTasksOptional, @ShellMainThread ShellExecutor mainExecutor) { @@ -230,7 +251,7 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static Optional<CompatUIController> provideCompatUIController( + static Optional<CompatUIHandler> provideCompatUIController( Context context, ShellInit shellInit, ShellController shellController, @@ -243,10 +264,25 @@ public abstract class WMShellBaseModule { Lazy<DockStateReader> dockStateReader, Lazy<CompatUIConfiguration> compatUIConfiguration, Lazy<CompatUIShellCommandHandler> compatUIShellCommandHandler, - Lazy<AccessibilityManager> accessibilityManager) { + Lazy<AccessibilityManager> accessibilityManager, + CompatUIRepository compatUIRepository, + Optional<DesktopModeTaskRepository> desktopModeTaskRepository, + @NonNull CompatUIState compatUIState, + @NonNull CompatUIComponentIdGenerator componentIdGenerator, + @NonNull CompatUIComponentFactory compatUIComponentFactory, + CompatUIStatusManager compatUIStatusManager) { if (!context.getResources().getBoolean(R.bool.config_enableCompatUIController)) { return Optional.empty(); } + if (Flags.appCompatUiFramework()) { + return Optional.of( + new DefaultCompatUIHandler(compatUIRepository, compatUIState, + componentIdGenerator, compatUIComponentFactory, mainExecutor)); + } + final IntPredicate inDesktopModePredicate = + desktopModeTaskRepository.<IntPredicate>map(modeTaskRepository -> displayId -> + modeTaskRepository.getVisibleTaskCount(displayId) > 0) + .orElseGet(() -> displayId -> false); return Optional.of( new CompatUIController( context, @@ -261,7 +297,53 @@ public abstract class WMShellBaseModule { dockStateReader.get(), compatUIConfiguration.get(), compatUIShellCommandHandler.get(), - accessibilityManager.get())); + accessibilityManager.get(), + compatUIStatusManager, + inDesktopModePredicate)); + } + + @WMSingleton + @Provides + static CompatUIStatusManager provideCompatUIStatusManager(@NonNull Context context) { + if (Flags.enableCompatUiVisibilityStatus()) { + return new CompatUIStatusManager( + newState -> Settings.Secure.putInt(context.getContentResolver(), + COMPAT_UI_EDUCATION_SHOWING, newState), + () -> Settings.Secure.getInt(context.getContentResolver(), + COMPAT_UI_EDUCATION_SHOWING, COMPAT_UI_EDUCATION_HIDDEN)); + } else { + return new CompatUIStatusManager(); + } + } + + @WMSingleton + @Provides + static CompatUIState provideCompatUIState() { + return new CompatUIState(); + } + + @WMSingleton + @Provides + static CompatUIComponentFactory provideCompatUIComponentFactory( + @NonNull Context context, + @NonNull SyncTransactionQueue syncQueue, + @NonNull DisplayController displayController) { + return new DefaultCompatUIComponentFactory(context, syncQueue, displayController); + } + + @WMSingleton + @Provides + static CompatUIComponentIdGenerator provideCompatUIComponentIdGenerator() { + return new DefaultComponentIdGenerator(); + } + + @WMSingleton + @Provides + static CompatUIRepository provideCompatUIRepository() { + // TODO(b/360288344) Integrate Dagger Multibinding + final CompatUIRepository repository = new DefaultCompatUIRepository(); + repository.addSpec(RestartButtonSpecKt.getRestartButtonSpec()); + return repository; } @WMSingleton @@ -361,7 +443,10 @@ public abstract class WMShellBaseModule { @ShellBackgroundThread Handler backgroundHandler, BackAnimationBackground backAnimationBackground, Optional<ShellBackAnimationRegistry> shellBackAnimationRegistry, - ShellCommandHandler shellCommandHandler) { + ShellCommandHandler shellCommandHandler, + Transitions transitions, + @ShellMainThread Handler handler + ) { if (BackAnimationController.IS_ENABLED) { return shellBackAnimationRegistry.map( (animations) -> @@ -373,7 +458,9 @@ public abstract class WMShellBaseModule { context, backAnimationBackground, animations, - shellCommandHandler)); + shellCommandHandler, + transitions, + handler)); } return Optional.empty(); } @@ -898,7 +985,7 @@ public abstract class WMShellBaseModule { // Use optional-of-lazy for the dependency that this provider relies on. // Lazy ensures that this provider will not be the cause the dependency is created // when it will not be returned due to the condition below. - return desktopTasksController.flatMap((lazy)-> { + return desktopTasksController.flatMap((lazy) -> { if (DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.of(lazy.get()); } @@ -917,7 +1004,7 @@ public abstract class WMShellBaseModule { // Use optional-of-lazy for the dependency that this provider relies on. // Lazy ensures that this provider will not be the cause the dependency is created // when it will not be returned due to the condition below. - return desktopModeTaskRepository.flatMap((lazy)-> { + return desktopModeTaskRepository.flatMap((lazy) -> { if (DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.of(lazy.get()); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellCoroutinesModule.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellCoroutinesModule.kt new file mode 100644 index 000000000000..cc47dbb78af2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellCoroutinesModule.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 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.dagger + +import android.os.Handler +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.annotations.ShellBackgroundThread +import com.android.wm.shell.shared.annotations.ShellMainThread +import dagger.Module +import dagger.Provides +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainCoroutineDispatcher +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.android.asCoroutineDispatcher +import kotlinx.coroutines.asCoroutineDispatcher + +/** + * Providers for various WmShell-specific coroutines-related constructs. + * + * Providers of [MainCoroutineDispatcher] intentionally creates the dispatcher with a [Handler] + * backing it instead of a [ShellExecutor] because [ShellExecutor.asCoroutineDispatcher] will + * create a [CoroutineDispatcher] whose [CoroutineDispatcher.isDispatchNeeded] is effectively never + * dispatching. This is because even if dispatched, the backing [ShellExecutor.execute] always runs + * the [Runnable] immediately if called from the same thread, whereas + * [Handler.asCoroutineDispatcher] will create a [MainCoroutineDispatcher] that correctly + * dispatches (queues) when [CoroutineDispatcher.isDispatchNeeded] is true using [Handler.post]. + * For callers that do need a non-dispatching version, [MainCoroutineDispatcher.immediate] is + * available. + */ +@Module +class WMShellCoroutinesModule { + @Provides + @ShellMainThread + fun provideMainDispatcher( + @ShellMainThread mainHandler: Handler + ): MainCoroutineDispatcher = mainHandler.asCoroutineDispatcher() + + @Provides + @ShellBackgroundThread + fun provideBackgroundDispatcher( + @ShellBackgroundThread backgroundHandler: Handler + ): MainCoroutineDispatcher = backgroundHandler.asCoroutineDispatcher() + + @Provides + @WMSingleton + @ShellMainThread + fun provideApplicationScope( + @ShellMainThread applicationDispatcher: MainCoroutineDispatcher, + ): CoroutineScope = CoroutineScope(applicationDispatcher) + + @Provides + @WMSingleton + @ShellBackgroundThread + fun provideBackgroundCoroutineScope( + @ShellBackgroundThread backgroundDispatcher: MainCoroutineDispatcher, + ): CoroutineScope = CoroutineScope(backgroundDispatcher) + + @Provides + @WMSingleton + @ShellBackgroundThread + fun provideBackgroundCoroutineContext( + @ShellBackgroundThread backgroundDispatcher: MainCoroutineDispatcher + ): CoroutineContext = backgroundDispatcher + SupervisorJob() +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 87bd84017dee..308bd0bccc95 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -16,7 +16,11 @@ package com.android.wm.shell.dagger; +import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT; + +import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.KeyguardManager; import android.content.Context; import android.content.pm.LauncherApps; import android.os.Handler; @@ -34,6 +38,8 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.activityembedding.ActivityEmbeddingController; +import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser; +import com.android.wm.shell.apptoweb.AssistContentRequester; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleData; import com.android.wm.shell.bubbles.BubbleDataRepository; @@ -52,9 +58,11 @@ import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TaskStackListenerImpl; -import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.dagger.back.ShellBackAnimationModule; import com.android.wm.shell.dagger.pip.PipModule; +import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler; +import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler; +import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler; import com.android.wm.shell.desktopmode.DesktopModeEventLogger; import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; @@ -64,7 +72,14 @@ import com.android.wm.shell.desktopmode.DesktopTasksTransitionObserver; import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler; +import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator; +import com.android.wm.shell.desktopmode.SpringDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler; +import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; +import com.android.wm.shell.desktopmode.education.AppHandleEducationController; +import com.android.wm.shell.desktopmode.education.AppHandleEducationFilter; +import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository; +import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.draganddrop.GlobalDragListener; import com.android.wm.shell.freeform.FreeformComponents; @@ -76,10 +91,11 @@ import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.recents.RecentsTransitionHandler; -import com.android.wm.shell.shared.DesktopModeStatus; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.annotations.ShellAnimationThread; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; @@ -101,12 +117,16 @@ import com.android.wm.shell.unfold.qualifier.UnfoldTransition; import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel; import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel; import com.android.wm.shell.windowdecor.WindowDecorViewModel; +import com.android.wm.shell.windowdecor.viewhost.DefaultWindowDecorViewHostSupplier; +import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier; import dagger.Binds; import dagger.Lazy; import dagger.Module; import dagger.Provides; +import kotlinx.coroutines.CoroutineScope; + import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -123,7 +143,7 @@ import java.util.Optional; includes = { WMShellBaseModule.class, PipModule.class, - ShellBackAnimationModule.class, + ShellBackAnimationModule.class }) public abstract class WMShellModule { @@ -156,8 +176,10 @@ public abstract class WMShellModule { BubbleLogger logger, BubblePositioner positioner, BubbleEducationController educationController, - @ShellMainThread ShellExecutor mainExecutor) { - return new BubbleData(context, logger, positioner, educationController, mainExecutor); + @ShellMainThread ShellExecutor mainExecutor, + @ShellBackgroundThread ShellExecutor bgExecutor) { + return new BubbleData(context, logger, positioner, educationController, mainExecutor, + bgExecutor); } // Note: Handler needed for LauncherApps.register @@ -190,7 +212,7 @@ public abstract class WMShellModule { IWindowManager wmService) { return new BubbleController(context, shellInit, shellCommandHandler, shellController, data, null /* synchronizer */, floatingContentCoordinator, - new BubbleDataRepository(launcherApps, mainExecutor, + new BubbleDataRepository(launcherApps, mainExecutor, bgExecutor, new BubblePersistentRepository(context)), statusBarService, windowManager, windowManagerShellWrapper, userManager, launcherApps, logger, taskStackListener, organizer, positioner, displayController, @@ -210,6 +232,7 @@ public abstract class WMShellModule { @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, @ShellMainThread Choreographer mainChoreographer, + @ShellBackgroundThread ShellExecutor bgExecutor, ShellInit shellInit, IWindowManager windowManager, ShellCommandHandler shellCommandHandler, @@ -220,13 +243,22 @@ public abstract class WMShellModule { SyncTransactionQueue syncQueue, Transitions transitions, Optional<DesktopTasksController> desktopTasksController, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + InteractionJankMonitor interactionJankMonitor, + AppToWebGenericLinksParser genericLinksParser, + AssistContentRequester assistContentRequester, + MultiInstanceHelper multiInstanceHelper, + Optional<DesktopTasksLimiter> desktopTasksLimiter, + WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, + Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler, + WindowDecorViewHostSupplier windowDecorViewHostSupplier) { if (DesktopModeStatus.canEnterDesktopMode(context)) { return new DesktopModeWindowDecorViewModel( context, mainExecutor, mainHandler, mainChoreographer, + bgExecutor, shellInit, shellCommandHandler, windowManager, @@ -237,17 +269,48 @@ public abstract class WMShellModule { syncQueue, transitions, desktopTasksController, - rootTaskDisplayAreaOrganizer); + rootTaskDisplayAreaOrganizer, + interactionJankMonitor, + genericLinksParser, + assistContentRequester, + multiInstanceHelper, + desktopTasksLimiter, + windowDecorCaptionHandleRepository, + desktopActivityOrientationHandler, + windowDecorViewHostSupplier); } return new CaptionWindowDecorViewModel( context, mainHandler, + mainExecutor, + bgExecutor, mainChoreographer, + windowManager, + shellInit, taskOrganizer, displayController, rootTaskDisplayAreaOrganizer, syncQueue, - transitions); + transitions, + windowDecorViewHostSupplier); + } + + @WMSingleton + @Provides + static AppToWebGenericLinksParser provideGenericLinksParser( + Context context, + @ShellMainThread ShellExecutor mainExecutor + ) { + return new AppToWebGenericLinksParser(context, mainExecutor); + } + + @Provides + static AssistContentRequester provideAssistContentRequester( + Context context, + @ShellMainThread ShellExecutor shellExecutor, + @ShellBackgroundThread ShellExecutor bgExecutor + ) { + return new AssistContentRequester(context, shellExecutor, bgExecutor); } // @@ -272,6 +335,7 @@ public abstract class WMShellModule { ShellInit shellInit, ShellTaskOrganizer shellTaskOrganizer, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, + LaunchAdjacentController launchAdjacentController, WindowDecorViewModel windowDecorViewModel) { // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic // override for this controller from the base module @@ -279,7 +343,7 @@ public abstract class WMShellModule { ? shellInit : null; return new FreeformTaskListener(context, init, shellTaskOrganizer, - desktopModeTaskRepository, windowDecorViewModel); + desktopModeTaskRepository, launchAdjacentController, windowDecorViewModel); } @WMSingleton @@ -291,9 +355,21 @@ public abstract class WMShellModule { WindowDecorViewModel windowDecorViewModel, DisplayController displayController, @ShellMainThread ShellExecutor mainExecutor, - @ShellAnimationThread ShellExecutor animExecutor) { - return new FreeformTaskTransitionHandler(shellInit, transitions, context, - windowDecorViewModel, displayController, mainExecutor, animExecutor); + @ShellAnimationThread ShellExecutor animExecutor, + @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, + InteractionJankMonitor interactionJankMonitor, + @ShellMainThread Handler handler) { + return new FreeformTaskTransitionHandler( + shellInit, + transitions, + context, + windowDecorViewModel, + displayController, + mainExecutor, + animExecutor, + desktopModeTaskRepository, + interactionJankMonitor, + handler); } @WMSingleton @@ -307,6 +383,13 @@ public abstract class WMShellModule { context, shellInit, transitions, windowDecorViewModel); } + @WMSingleton + @Provides + static WindowDecorViewHostSupplier provideWindowDecorViewHostSupplier( + @ShellMainThread @NonNull CoroutineScope mainScope) { + return new DefaultWindowDecorViewHostSupplier(mainScope); + } + // // One handed mode // @@ -360,13 +443,14 @@ public abstract class WMShellModule { Optional<WindowDecorViewModel> windowDecorViewModel, Optional<DesktopTasksController> desktopTasksController, MultiInstanceHelper multiInstanceHelper, - @ShellMainThread ShellExecutor mainExecutor) { + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler) { return new SplitScreenController(context, shellInit, shellCommandHandler, shellController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, displayController, displayImeController, displayInsetsController, dragAndDropController, transitions, transactionPool, iconProvider, recentTasks, launchAdjacentController, windowDecorViewModel, desktopTasksController, null /* stageCoordinator */, - multiInstanceHelper, mainExecutor); + multiInstanceHelper, mainExecutor, mainHandler); } // @@ -396,10 +480,11 @@ public abstract class WMShellModule { @Provides static RecentsTransitionHandler provideRecentsTransitionHandler( ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, Transitions transitions, Optional<RecentTasksController> recentTasksController, HomeTransitionObserver homeTransitionObserver) { - return new RecentsTransitionHandler(shellInit, transitions, + return new RecentsTransitionHandler(shellInit, shellTaskOrganizer, transitions, recentTasksController.orElse(null), homeTransitionObserver); } @@ -512,8 +597,11 @@ public abstract class WMShellModule { RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, DragAndDropController dragAndDropController, Transitions transitions, + KeyguardManager keyguardManager, + ReturnToDragStartAnimator returnToDragStartAnimator, EnterDesktopTaskTransitionHandler enterDesktopTransitionHandler, ExitDesktopTaskTransitionHandler exitDesktopTransitionHandler, + DesktopModeDragAndDropTransitionHandler desktopModeDragAndDropTransitionHandler, ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, DragToDesktopTransitionHandler dragToDesktopTransitionHandler, @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, @@ -522,16 +610,20 @@ public abstract class WMShellModule { RecentsTransitionHandler recentsTransitionHandler, MultiInstanceHelper multiInstanceHelper, @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler, Optional<DesktopTasksLimiter> desktopTasksLimiter, - Optional<RecentTasksController> recentTasksController) { + Optional<RecentTasksController> recentTasksController, + InteractionJankMonitor interactionJankMonitor) { return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController, displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, - dragAndDropController, transitions, enterDesktopTransitionHandler, - exitDesktopTransitionHandler, toggleResizeDesktopTaskTransitionHandler, + dragAndDropController, transitions, keyguardManager, + returnToDragStartAnimator, enterDesktopTransitionHandler, + exitDesktopTransitionHandler, desktopModeDragAndDropTransitionHandler, + toggleResizeDesktopTaskTransitionHandler, dragToDesktopTransitionHandler, desktopModeTaskRepository, desktopModeLoggerTransitionObserver, launchAdjacentController, - recentsTransitionHandler, multiInstanceHelper, - mainExecutor, desktopTasksLimiter, recentTasksController.orElse(null)); + recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter, + recentTasksController.orElse(null), interactionJankMonitor, mainHandler); } @WMSingleton @@ -540,14 +632,32 @@ public abstract class WMShellModule { Context context, Transitions transitions, @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, - ShellTaskOrganizer shellTaskOrganizer) { + ShellTaskOrganizer shellTaskOrganizer, + InteractionJankMonitor interactionJankMonitor, + @ShellMainThread Handler handler) { + int maxTaskLimit = DesktopModeStatus.getMaxTaskLimit(context); if (!DesktopModeStatus.canEnterDesktopMode(context) - || !Flags.enableDesktopWindowingTaskLimit()) { + || !ENABLE_DESKTOP_WINDOWING_TASK_LIMIT.isTrue() + || maxTaskLimit <= 0) { return Optional.empty(); } return Optional.of( new DesktopTasksLimiter( - transitions, desktopModeTaskRepository, shellTaskOrganizer)); + transitions, + desktopModeTaskRepository, + shellTaskOrganizer, + maxTaskLimit, + interactionJankMonitor, + context, + handler) + ); + } + + @WMSingleton + @Provides + static ReturnToDragStartAnimator provideReturnToDragStartAnimator( + Context context, InteractionJankMonitor interactionJankMonitor) { + return new ReturnToDragStartAnimator(context, interactionJankMonitor); } @@ -557,40 +667,78 @@ public abstract class WMShellModule { Context context, Transitions transitions, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, - Optional<DesktopTasksLimiter> desktopTasksLimiter) { - return new DragToDesktopTransitionHandler(context, transitions, - rootTaskDisplayAreaOrganizer); + InteractionJankMonitor interactionJankMonitor) { + return Flags.enableDesktopWindowingTransitions() + ? new SpringDragToDesktopTransitionHandler(context, transitions, + rootTaskDisplayAreaOrganizer, interactionJankMonitor) + : new DefaultDragToDesktopTransitionHandler(context, transitions, + rootTaskDisplayAreaOrganizer, interactionJankMonitor); } @WMSingleton @Provides static EnterDesktopTaskTransitionHandler provideEnterDesktopModeTaskTransitionHandler( Transitions transitions, - Optional<DesktopTasksLimiter> desktopTasksLimiter) { - return new EnterDesktopTaskTransitionHandler(transitions); + Optional<DesktopTasksLimiter> desktopTasksLimiter, + InteractionJankMonitor interactionJankMonitor) { + return new EnterDesktopTaskTransitionHandler(transitions, interactionJankMonitor); } @WMSingleton @Provides static ToggleResizeDesktopTaskTransitionHandler provideToggleResizeDesktopTaskTransitionHandler( - Transitions transitions) { - return new ToggleResizeDesktopTaskTransitionHandler(transitions); + Transitions transitions, InteractionJankMonitor interactionJankMonitor) { + return new ToggleResizeDesktopTaskTransitionHandler(transitions, interactionJankMonitor); } @WMSingleton @Provides static ExitDesktopTaskTransitionHandler provideExitDesktopTaskTransitionHandler( Transitions transitions, - Context context + Context context, + InteractionJankMonitor interactionJankMonitor, + @ShellMainThread Handler handler) { + return new ExitDesktopTaskTransitionHandler( + transitions, context, interactionJankMonitor, handler); + } + + @WMSingleton + @Provides + static DesktopModeDragAndDropTransitionHandler provideDesktopModeDragAndDropTransitionHandler( + Transitions transitions ) { - return new ExitDesktopTaskTransitionHandler(transitions, context); + return new DesktopModeDragAndDropTransitionHandler(transitions); } @WMSingleton @Provides @DynamicOverride - static DesktopModeTaskRepository provideDesktopModeTaskRepository() { - return new DesktopModeTaskRepository(); + static DesktopModeTaskRepository provideDesktopModeTaskRepository( + Context context, + ShellInit shellInit, + DesktopPersistentRepository desktopPersistentRepository, + @ShellMainThread CoroutineScope mainScope + ) { + return new DesktopModeTaskRepository(context, shellInit, desktopPersistentRepository, + mainScope); + } + + @WMSingleton + @Provides + static Optional<DesktopActivityOrientationChangeHandler> provideActivityOrientationHandler( + Context context, + ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, + TaskStackListenerImpl taskStackListener, + ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, + @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository + ) { + if (DesktopModeStatus.canEnterDesktopMode(context)) { + return Optional.of(new DesktopActivityOrientationChangeHandler( + context, shellInit, shellTaskOrganizer, taskStackListener, + toggleResizeDesktopTaskTransitionHandler, desktopModeTaskRepository)); + } + return Optional.empty(); } @WMSingleton @@ -599,11 +747,12 @@ public abstract class WMShellModule { Context context, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, Transitions transitions, + ShellTaskOrganizer shellTaskOrganizer, ShellInit shellInit ) { return desktopModeTaskRepository.flatMap(repository -> Optional.of(new DesktopTasksTransitionObserver( - context, repository, transitions, shellInit)) + context, repository, transitions, shellTaskOrganizer, shellInit)) ); } @@ -624,6 +773,46 @@ public abstract class WMShellModule { return new DesktopModeEventLogger(); } + @WMSingleton + @Provides + static AppHandleEducationDatastoreRepository provideAppHandleEducationDatastoreRepository( + Context context) { + return new AppHandleEducationDatastoreRepository(context); + } + + @WMSingleton + @Provides + static AppHandleEducationFilter provideAppHandleEducationFilter( + Context context, + AppHandleEducationDatastoreRepository appHandleEducationDatastoreRepository) { + return new AppHandleEducationFilter(context, appHandleEducationDatastoreRepository); + } + + @WMSingleton + @Provides + static WindowDecorCaptionHandleRepository provideAppHandleRepository() { + return new WindowDecorCaptionHandleRepository(); + } + + @WMSingleton + @Provides + static AppHandleEducationController provideAppHandleEducationController( + AppHandleEducationFilter appHandleEducationFilter, + ShellTaskOrganizer shellTaskOrganizer, + AppHandleEducationDatastoreRepository appHandleEducationDatastoreRepository, + @ShellMainThread CoroutineScope applicationScope) { + return new AppHandleEducationController(appHandleEducationFilter, + shellTaskOrganizer, appHandleEducationDatastoreRepository, applicationScope); + } + + @WMSingleton + @Provides + static DesktopPersistentRepository provideDesktopPersistentRepository( + Context context, + @ShellBackgroundThread CoroutineScope bgScope) { + return new DesktopPersistentRepository(context, bgScope); + } + // // Drag and drop // @@ -642,6 +831,7 @@ public abstract class WMShellModule { ShellInit shellInit, ShellController shellController, ShellCommandHandler shellCommandHandler, + ShellTaskOrganizer shellTaskOrganizer, DisplayController displayController, UiEventLogger uiEventLogger, IconProvider iconProvider, @@ -649,8 +839,8 @@ public abstract class WMShellModule { Transitions transitions, @ShellMainThread ShellExecutor mainExecutor) { return new DragAndDropController(context, shellInit, shellController, shellCommandHandler, - displayController, uiEventLogger, iconProvider, globalDragListener, transitions, - mainExecutor); + shellTaskOrganizer, displayController, uiEventLogger, iconProvider, + globalDragListener, transitions, mainExecutor); } // @@ -664,7 +854,8 @@ public abstract class WMShellModule { @Provides static Object provideIndependentShellComponentsToCreate( DragAndDropController dragAndDropController, - Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional + Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional, + AppHandleEducationController appHandleEducationController ) { return new Object(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java index 677fd5deffd3..3a4764d45f2c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java @@ -38,12 +38,10 @@ import com.android.wm.shell.common.pip.PipMediaController; import com.android.wm.shell.common.pip.PipPerfHintController; import com.android.wm.shell.common.pip.PipSnapAlgorithm; import com.android.wm.shell.common.pip.PipUiEventLogger; -import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.dagger.WMShellBaseModule; import com.android.wm.shell.dagger.WMSingleton; import com.android.wm.shell.onehanded.OneHandedController; -import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; @@ -79,7 +77,7 @@ import java.util.Optional; public abstract class Pip1Module { @WMSingleton @Provides - static Optional<Pip> providePip1(Context context, + static Optional<PipController.PipImpl> providePip1(Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, ShellController shellController, @@ -103,21 +101,18 @@ public abstract class Pip1Module { DisplayInsetsController displayInsetsController, TabletopModeController pipTabletopController, Optional<OneHandedController> oneHandedController, - @ShellMainThread ShellExecutor mainExecutor) { - if (PipUtils.isPip2ExperimentEnabled()) { - return Optional.empty(); - } else { - return Optional.ofNullable(PipController.create( - context, shellInit, shellCommandHandler, shellController, - displayController, pipAnimationController, pipAppOpsListener, - pipBoundsAlgorithm, - pipKeepClearAlgorithm, pipBoundsState, pipDisplayLayoutState, - pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer, - pipTransitionState, pipTouchHandler, pipTransitionController, - windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, - displayInsetsController, pipTabletopController, oneHandedController, - mainExecutor)); - } + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler handler) { + return Optional.ofNullable(PipController.create( + context, shellInit, shellCommandHandler, shellController, + displayController, pipAnimationController, pipAppOpsListener, + pipBoundsAlgorithm, + pipKeepClearAlgorithm, pipBoundsState, pipDisplayLayoutState, + pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer, + pipTransitionState, pipTouchHandler, pipTransitionController, + windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, + displayInsetsController, pipTabletopController, oneHandedController, + mainExecutor, handler)); } // Handler is used by Icon.loadDrawableAsync @@ -212,12 +207,13 @@ public abstract class Pip1Module { @WMSingleton @Provides static PipMotionHelper providePipMotionHelper(Context context, + @ShellMainThread ShellExecutor mainExecutor, PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer, PhonePipMenuController menuController, PipSnapAlgorithm pipSnapAlgorithm, PipTransitionController pipTransitionController, FloatingContentCoordinator floatingContentCoordinator, Optional<PipPerfHintController> pipPerfHintControllerOptional) { - return new PipMotionHelper(context, pipBoundsState, pipTaskOrganizer, + return new PipMotionHelper(context, mainExecutor, pipBoundsState, pipTaskOrganizer, menuController, pipSnapAlgorithm, pipTransitionController, floatingContentCoordinator, pipPerfHintControllerOptional); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 696831747865..3464fef07f33 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -42,9 +42,11 @@ import com.android.wm.shell.pip2.phone.PhonePipMenuController; import com.android.wm.shell.pip2.phone.PipController; import com.android.wm.shell.pip2.phone.PipMotionHelper; import com.android.wm.shell.pip2.phone.PipScheduler; +import com.android.wm.shell.pip2.phone.PipTaskListener; import com.android.wm.shell.pip2.phone.PipTouchHandler; import com.android.wm.shell.pip2.phone.PipTransition; import com.android.wm.shell.pip2.phone.PipTransitionState; +import com.android.wm.shell.pip2.phone.PipUiStateChangeController; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; @@ -72,11 +74,23 @@ public abstract class Pip2Module { PipBoundsAlgorithm pipBoundsAlgorithm, Optional<PipController> pipController, PipTouchHandler pipTouchHandler, + PipTaskListener pipTaskListener, @NonNull PipScheduler pipScheduler, - @NonNull PipTransitionState pipStackListenerController) { + @NonNull PipTransitionState pipStackListenerController, + @NonNull PipUiStateChangeController pipUiStateChangeController) { return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, - pipBoundsState, null, pipBoundsAlgorithm, pipScheduler, - pipStackListenerController); + pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener, + pipScheduler, pipStackListenerController, pipUiStateChangeController); + } + + @WMSingleton + @Provides + static Optional<PipController.PipImpl> providePip2(Optional<PipController> pipController) { + if (pipController.isEmpty()) { + return Optional.empty(); + } else { + return Optional.ofNullable(pipController.get().getPipImpl()); + } } @WMSingleton @@ -94,6 +108,7 @@ public abstract class Pip2Module { TaskStackListenerImpl taskStackListener, ShellTaskOrganizer shellTaskOrganizer, PipTransitionState pipTransitionState, + PipTouchHandler pipTouchHandler, @ShellMainThread ShellExecutor mainExecutor) { if (!PipUtils.isPip2ExperimentEnabled()) { return Optional.empty(); @@ -102,7 +117,7 @@ public abstract class Pip2Module { context, shellInit, shellCommandHandler, shellController, displayController, displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer, - pipTransitionState, mainExecutor)); + pipTransitionState, pipTouchHandler, mainExecutor)); } } @@ -110,9 +125,11 @@ public abstract class Pip2Module { @Provides static PipScheduler providePipScheduler(Context context, PipBoundsState pipBoundsState, + PhonePipMenuController pipMenuController, @ShellMainThread ShellExecutor mainExecutor, PipTransitionState pipTransitionState) { - return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState); + return new PipScheduler(context, pipBoundsState, pipMenuController, + mainExecutor, pipTransitionState); } @WMSingleton @@ -170,4 +187,24 @@ public abstract class Pip2Module { static PipTransitionState providePipTransitionState(@ShellMainThread Handler handler) { return new PipTransitionState(handler); } + + @WMSingleton + @Provides + static PipUiStateChangeController providePipUiStateChangeController( + PipTransitionState pipTransitionState) { + return new PipUiStateChangeController(pipTransitionState); + } + + @WMSingleton + @Provides + static PipTaskListener providePipTaskListener(Context context, + ShellTaskOrganizer shellTaskOrganizer, + PipTransitionState pipTransitionState, + PipScheduler pipScheduler, + PipBoundsState pipBoundsState, + PipBoundsAlgorithm pipBoundsAlgorithm, + @ShellMainThread ShellExecutor mainExecutor) { + return new PipTaskListener(context, shellTaskOrganizer, pipTransitionState, + pipScheduler, pipBoundsState, pipBoundsAlgorithm, mainExecutor); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java index f2631eff890d..a3afe7860f2e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java @@ -18,12 +18,16 @@ package com.android.wm.shell.dagger.pip; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.dagger.WMSingleton; +import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.pip2.phone.PipController; import com.android.wm.shell.pip2.phone.PipTransition; import dagger.Module; import dagger.Provides; +import java.util.Optional; + /** * Provides dependencies for external components / modules reference PiP and extracts away the * selection of legacy and new PiP implementation. @@ -44,4 +48,17 @@ public abstract class PipModule { return legacyPipTransition; } } + + @WMSingleton + @Provides + static Optional<Pip> providePip( + Optional<com.android.wm.shell.pip.phone.PipController.PipImpl> pip1, + Optional<PipController.PipImpl> pip2) { + if (PipUtils.isPip2ExperimentEnabled()) { + return Optional.ofNullable(pip2.orElse(null)); + + } else { + return Optional.ofNullable(pip1.orElse(null)); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt new file mode 100644 index 000000000000..59e006879da8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2024 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.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.content.Context +import android.content.pm.ActivityInfo +import android.content.pm.ActivityInfo.ScreenOrientation +import android.content.res.Configuration.ORIENTATION_LANDSCAPE +import android.content.res.Configuration.ORIENTATION_PORTRAIT +import android.graphics.Rect +import android.util.Size +import android.window.WindowContainerTransaction +import com.android.window.flags.Flags +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.common.TaskStackListenerCallback +import com.android.wm.shell.common.TaskStackListenerImpl +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.sysui.ShellInit + +/** Handles task resizing to respect orientation change of non-resizeable activities in desktop. */ +class DesktopActivityOrientationChangeHandler( + context: Context, + shellInit: ShellInit, + private val shellTaskOrganizer: ShellTaskOrganizer, + private val taskStackListener: TaskStackListenerImpl, + private val resizeHandler: ToggleResizeDesktopTaskTransitionHandler, + private val taskRepository: DesktopModeTaskRepository, +) { + + init { + if (DesktopModeStatus.canEnterDesktopMode(context)) { + shellInit.addInitCallback({ onInit() }, this) + } + } + + private fun onInit() { + taskStackListener.addListener(object : TaskStackListenerCallback { + override fun onActivityRequestedOrientationChanged( + taskId: Int, + @ScreenOrientation requestedOrientation: Int + ) { + // Handle requested screen orientation changes at runtime. + handleActivityOrientationChange(taskId, requestedOrientation) + } + }) + } + + /** + * Triggered with onTaskInfoChanged to handle: + * * New activity launching from same task with different orientation + * * Top activity closing in same task with different orientation to previous activity + */ + fun handleActivityOrientationChange(oldTask: RunningTaskInfo, newTask: RunningTaskInfo) { + val newTopActivityInfo = newTask.topActivityInfo ?: return + val oldTopActivityInfo = oldTask.topActivityInfo ?: return + // Check if screen orientation is different from old task info so there is no duplicated + // calls to handle runtime requested orientation changes. + if (oldTopActivityInfo.screenOrientation != newTopActivityInfo.screenOrientation) { + handleActivityOrientationChange(newTask.taskId, newTopActivityInfo.screenOrientation) + } + } + + private fun handleActivityOrientationChange( + taskId: Int, + @ScreenOrientation requestedOrientation: Int + ) { + if (!Flags.respectOrientationChangeForUnresizeable()) return + val task = shellTaskOrganizer.getRunningTaskInfo(taskId) ?: return + if (!isDesktopModeShowing(task.displayId) || !task.isFreeform || task.isResizeable) return + + val taskBounds = task.configuration.windowConfiguration.bounds + val taskHeight = taskBounds.height() + val taskWidth = taskBounds.width() + if (taskWidth == taskHeight) return + val orientation = + if (taskWidth > taskHeight) ORIENTATION_LANDSCAPE else ORIENTATION_PORTRAIT + + // Non-resizeable activity requested opposite orientation. + if (orientation == ORIENTATION_PORTRAIT + && ActivityInfo.isFixedOrientationLandscape(requestedOrientation) + || orientation == ORIENTATION_LANDSCAPE + && ActivityInfo.isFixedOrientationPortrait(requestedOrientation)) { + + val finalSize = Size(taskHeight, taskWidth) + // Use the center x as the resizing anchor point. + val left = taskBounds.centerX() - finalSize.width / 2 + val right = left + finalSize.width + val finalBounds = Rect(left, taskBounds.top, right, taskBounds.top + finalSize.height) + + val wct = WindowContainerTransaction().setBounds(task.token, finalBounds) + resizeHandler.startTransition(wct) + } + } + + private fun isDesktopModeShowing(displayId: Int): Boolean = + taskRepository.getVisibleTaskCount(displayId) > 0 +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java index 31c8f1e45007..cca750014fc1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java @@ -18,8 +18,8 @@ package com.android.wm.shell.desktopmode; import android.graphics.Region; -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.shared.annotations.ExternalThread; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import java.util.concurrent.Executor; import java.util.function.Consumer; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt new file mode 100644 index 000000000000..a7a4a1036b5d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2024 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.desktopmode + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.os.IBinder +import android.view.SurfaceControl +import android.view.WindowManager.TRANSIT_OPEN +import android.window.TransitionInfo +import android.window.TransitionRequestInfo +import android.window.WindowContainerTransaction +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TransitionFinishCallback + +/** + * Transition handler for drag-and-drop (i.e., tab tear) transitions that occur in desktop mode. + */ +class DesktopModeDragAndDropTransitionHandler(private val transitions: Transitions) : + Transitions.TransitionHandler { + private val pendingTransitionTokens: MutableList<IBinder> = mutableListOf() + + /** + * Begin a transition when a [android.app.PendingIntent] is dropped without a window to + * accept it. + */ + fun handleDropEvent(wct: WindowContainerTransaction): IBinder { + val token = transitions.startTransition(TRANSIT_OPEN, wct, this) + pendingTransitionTokens.add(token) + return token + } + + override fun startAnimation( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction, + finishCallback: TransitionFinishCallback + ): Boolean { + if (!pendingTransitionTokens.contains(transition)) return false + val change = findRelevantChange(info) + val leash = change.leash + val endBounds = change.endAbsBounds + startTransaction.hide(leash) + .setWindowCrop(leash, endBounds.width(), endBounds.height()) + .apply() + val animator = ValueAnimator() + animator.setFloatValues(0f, 1f) + animator.setDuration(FADE_IN_ANIMATION_DURATION) + val t = SurfaceControl.Transaction() + animator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + t.show(leash) + t.apply() + } + + override fun onAnimationEnd(animation: Animator) { + finishCallback.onTransitionFinished(null) + } + }) + animator.addUpdateListener { animation: ValueAnimator -> + t.setAlpha(leash, animation.animatedFraction) + t.apply() + } + animator.start() + pendingTransitionTokens.remove(transition) + return true + } + + private fun findRelevantChange(info: TransitionInfo): TransitionInfo.Change { + val matchingChanges = + info.changes.filter { c -> + isValidTaskChange(c) && c.mode == TRANSIT_OPEN + } + if (matchingChanges.size != 1) { + throw IllegalStateException( + "Expected 1 relevant change but found: ${matchingChanges.size}" + ) + } + return matchingChanges.first() + } + + private fun isValidTaskChange(change: TransitionInfo.Change): Boolean { + return change.taskInfo != null && change.taskInfo?.taskId != -1 + } + + override fun handleRequest( + transition: IBinder, + request: TransitionRequestInfo + ): WindowContainerTransaction? { + return null + } + + companion object { + const val FADE_IN_ANIMATION_DURATION = 300L + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt index 9192e6ed3175..02cbe01d0a03 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt @@ -16,9 +16,10 @@ package com.android.wm.shell.desktopmode +import com.android.internal.annotations.VisibleForTesting +import com.android.internal.protolog.ProtoLog import com.android.internal.util.FrameworkStatsLog import com.android.wm.shell.protolog.ShellProtoLogGroup -import com.android.wm.shell.util.KtProtoLog /** Event logger for logging desktop mode session events */ class DesktopModeEventLogger { @@ -27,7 +28,7 @@ class DesktopModeEventLogger { * entering desktop mode */ fun logSessionEnter(sessionId: Int, enterReason: EnterReason) { - KtProtoLog.v( + ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DesktopModeLogger: Logging session enter, session: %s reason: %s", sessionId, @@ -47,7 +48,7 @@ class DesktopModeEventLogger { * exiting desktop mode */ fun logSessionExit(sessionId: Int, exitReason: ExitReason) { - KtProtoLog.v( + ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DesktopModeLogger: Logging session exit, session: %s reason: %s", sessionId, @@ -67,31 +68,15 @@ class DesktopModeEventLogger { * session id [sessionId] */ fun logTaskAdded(sessionId: Int, taskUpdate: TaskUpdate) { - KtProtoLog.v( + ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DesktopModeLogger: Logging task added, session: %s taskId: %s", sessionId, taskUpdate.instanceId ) - FrameworkStatsLog.write( - DESKTOP_MODE_TASK_UPDATE_ATOM_ID, - /* task_event */ + logTaskUpdate( FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED, - /* instance_id */ - taskUpdate.instanceId, - /* uid */ - taskUpdate.uid, - /* task_height */ - taskUpdate.taskHeight, - /* task_width */ - taskUpdate.taskWidth, - /* task_x */ - taskUpdate.taskX, - /* task_y */ - taskUpdate.taskY, - /* session_id */ - sessionId - ) + sessionId, taskUpdate) } /** @@ -99,31 +84,15 @@ class DesktopModeEventLogger { * session id [sessionId] */ fun logTaskRemoved(sessionId: Int, taskUpdate: TaskUpdate) { - KtProtoLog.v( + ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DesktopModeLogger: Logging task remove, session: %s taskId: %s", sessionId, taskUpdate.instanceId ) - FrameworkStatsLog.write( - DESKTOP_MODE_TASK_UPDATE_ATOM_ID, - /* task_event */ + logTaskUpdate( FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED, - /* instance_id */ - taskUpdate.instanceId, - /* uid */ - taskUpdate.uid, - /* task_height */ - taskUpdate.taskHeight, - /* task_width */ - taskUpdate.taskWidth, - /* task_x */ - taskUpdate.taskX, - /* task_y */ - taskUpdate.taskY, - /* session_id */ - sessionId - ) + sessionId, taskUpdate) } /** @@ -131,16 +100,22 @@ class DesktopModeEventLogger { * having session id [sessionId] */ fun logTaskInfoChanged(sessionId: Int, taskUpdate: TaskUpdate) { - KtProtoLog.v( + ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DesktopModeLogger: Logging task info changed, session: %s taskId: %s", sessionId, taskUpdate.instanceId ) + logTaskUpdate( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED, + sessionId, taskUpdate) + } + + private fun logTaskUpdate(taskEvent: Int, sessionId: Int, taskUpdate: TaskUpdate) { FrameworkStatsLog.write( DESKTOP_MODE_TASK_UPDATE_ATOM_ID, /* task_event */ - FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED, + taskEvent, /* instance_id */ taskUpdate.instanceId, /* uid */ @@ -154,20 +129,82 @@ class DesktopModeEventLogger { /* task_y */ taskUpdate.taskY, /* session_id */ - sessionId + sessionId, + taskUpdate.minimizeReason?.reason ?: UNSET_MINIMIZE_REASON, + taskUpdate.unminimizeReason?.reason ?: UNSET_UNMINIMIZE_REASON, + /* visible_task_count */ + taskUpdate.visibleTaskCount ) } companion object { + /** + * Describes a task position and dimensions. + * + * @property instanceId instance id of the task + * @property uid uid of the app associated with the task + * @property taskHeight height of the task in px + * @property taskWidth width of the task in px + * @property taskX x-coordinate of the top-left corner + * @property taskY y-coordinate of the top-left corner + * @property minimizeReason the reason the task was minimized + * @property unminimizeEvent the reason the task was unminimized + * + */ data class TaskUpdate( val instanceId: Int, val uid: Int, - val taskHeight: Int = Int.MIN_VALUE, - val taskWidth: Int = Int.MIN_VALUE, - val taskX: Int = Int.MIN_VALUE, - val taskY: Int = Int.MIN_VALUE, + val taskHeight: Int, + val taskWidth: Int, + val taskX: Int, + val taskY: Int, + val minimizeReason: MinimizeReason? = null, + val unminimizeReason: UnminimizeReason? = null, + val visibleTaskCount: Int, ) + // Default value used when the task was not minimized. + @VisibleForTesting + const val UNSET_MINIMIZE_REASON = + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__MINIMIZE_REASON__UNSET_MINIMIZE + + /** The reason a task was minimized. */ + enum class MinimizeReason (val reason: Int) { + TASK_LIMIT( + FrameworkStatsLog + .DESKTOP_MODE_SESSION_TASK_UPDATE__MINIMIZE_REASON__MINIMIZE_TASK_LIMIT + ), + MINIMIZE_BUTTON( // TODO(b/356843241): use this enum value + FrameworkStatsLog + .DESKTOP_MODE_SESSION_TASK_UPDATE__MINIMIZE_REASON__MINIMIZE_BUTTON + ), + } + + // Default value used when the task was not unminimized. + @VisibleForTesting + const val UNSET_UNMINIMIZE_REASON = + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__UNMINIMIZE_REASON__UNSET_UNMINIMIZE + + /** The reason a task was unminimized. */ + enum class UnminimizeReason (val reason: Int) { + UNKNOWN( + FrameworkStatsLog + .DESKTOP_MODE_SESSION_TASK_UPDATE__UNMINIMIZE_REASON__UNMINIMIZE_UNKNOWN + ), + TASKBAR_TAP( + FrameworkStatsLog + .DESKTOP_MODE_SESSION_TASK_UPDATE__UNMINIMIZE_REASON__UNMINIMIZE_TASKBAR_TAP + ), + ALT_TAB( + FrameworkStatsLog + .DESKTOP_MODE_SESSION_TASK_UPDATE__UNMINIMIZE_REASON__UNMINIMIZE_ALT_TAB + ), + TASK_LAUNCH( + FrameworkStatsLog + .DESKTOP_MODE_SESSION_TASK_UPDATE__UNMINIMIZE_REASON__UNMINIMIZE_TASK_LAUNCH + ), + } + /** * Enum EnterReason mapped to the EnterReason definition in * stats/atoms/desktopmode/desktopmode_extensions_atoms.proto diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt index 641952b28bfb..063747494a82 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt @@ -22,6 +22,8 @@ import android.app.TaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.Context import android.os.IBinder +import android.os.SystemProperties +import android.os.Trace import android.util.SparseArray import android.view.SurfaceControl import android.view.WindowManager @@ -35,7 +37,7 @@ import androidx.core.util.plus import androidx.core.util.putAll import com.android.internal.logging.InstanceId import com.android.internal.logging.InstanceIdSequence -import com.android.internal.protolog.common.ProtoLog +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.TaskUpdate @@ -46,11 +48,10 @@ import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_ import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE -import com.android.wm.shell.shared.DesktopModeStatus +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions -import com.android.wm.shell.util.KtProtoLog /** * A [Transitions.TransitionObserver] that observes transitions and the proposed changes to log @@ -84,6 +85,10 @@ class DesktopModeLoggerTransitionObserver( // Caching whether the previous transition was exit to overview. private var wasPreviousTransitionExitToOverview: Boolean = false + // Caching whether the previous transition was exit due to screen off. This helps check if a + // following enter reason could be Screen On + private var wasPreviousTransitionExitByScreenOff: Boolean = false + // The instanceId for the current logging session private var loggerInstanceId: InstanceId? = null @@ -106,7 +111,7 @@ class DesktopModeLoggerTransitionObserver( ) { // this was a new recents animation if (info.isExitToRecentsTransition() && tasksSavedForRecents.isEmpty()) { - KtProtoLog.v( + ProtoLog.v( WM_SHELL_DESKTOP_MODE, "DesktopModeLogger: Recents animation running, saving tasks for later" ) @@ -132,7 +137,7 @@ class DesktopModeLoggerTransitionObserver( info.flags == 0 && tasksSavedForRecents.isNotEmpty() ) { - KtProtoLog.v( + ProtoLog.v( WM_SHELL_DESKTOP_MODE, "DesktopModeLogger: Canceled recents animation, restoring tasks" ) @@ -202,7 +207,7 @@ class DesktopModeLoggerTransitionObserver( } } - KtProtoLog.v( + ProtoLog.v( WM_SHELL_DESKTOP_MODE, "DesktopModeLogger: taskInfo map after processing changes %s", postTransitionFreeformTasks.size() @@ -282,41 +287,73 @@ class DesktopModeLoggerTransitionObserver( visibleFreeformTaskInfos.putAll(postTransitionVisibleFreeformTasks) } - // TODO(b/326231724) - Add logging around taskInfoChanges Updates /** Compare the old and new state of taskInfos and identify and log the changes */ private fun identifyAndLogTaskUpdates( sessionId: Int, preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>, postTransitionVisibleFreeformTasks: SparseArray<TaskInfo> ) { - // find new tasks that were added postTransitionVisibleFreeformTasks.forEach { taskId, taskInfo -> - if (!preTransitionVisibleFreeformTasks.containsKey(taskId)) { - desktopModeEventLogger.logTaskAdded(sessionId, buildTaskUpdateForTask(taskInfo)) + val currentTaskUpdate = buildTaskUpdateForTask(taskInfo, + postTransitionVisibleFreeformTasks.size()) + val previousTaskInfo = preTransitionVisibleFreeformTasks[taskId] + when { + // new tasks added + previousTaskInfo == null -> { + desktopModeEventLogger.logTaskAdded(sessionId, currentTaskUpdate) + Trace.setCounter( + Trace.TRACE_TAG_WINDOW_MANAGER, + VISIBLE_TASKS_COUNTER_NAME, + postTransitionVisibleFreeformTasks.size().toLong() + ) + SystemProperties.set(VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY, + postTransitionVisibleFreeformTasks.size().toString()) + } + // old tasks that were resized or repositioned + // TODO(b/347935387): Log changes only once they are stable. + buildTaskUpdateForTask(previousTaskInfo, postTransitionVisibleFreeformTasks.size()) + != currentTaskUpdate -> + desktopModeEventLogger.logTaskInfoChanged(sessionId, currentTaskUpdate) } } // find old tasks that were removed preTransitionVisibleFreeformTasks.forEach { taskId, taskInfo -> if (!postTransitionVisibleFreeformTasks.containsKey(taskId)) { - desktopModeEventLogger.logTaskRemoved(sessionId, buildTaskUpdateForTask(taskInfo)) + desktopModeEventLogger.logTaskRemoved(sessionId, + buildTaskUpdateForTask(taskInfo, postTransitionVisibleFreeformTasks.size())) + Trace.setCounter( + Trace.TRACE_TAG_WINDOW_MANAGER, + VISIBLE_TASKS_COUNTER_NAME, + postTransitionVisibleFreeformTasks.size().toLong() + ) + SystemProperties.set(VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY, + postTransitionVisibleFreeformTasks.size().toString()) } } } - // TODO(b/326231724: figure out how to get taskWidth and taskHeight from TaskInfo - private fun buildTaskUpdateForTask(taskInfo: TaskInfo): TaskUpdate { - val taskUpdate = TaskUpdate(taskInfo.taskId, taskInfo.userId) - // add task x, y if available - taskInfo.positionInParent?.let { taskUpdate.copy(taskX = it.x, taskY = it.y) } - - return taskUpdate + private fun buildTaskUpdateForTask(taskInfo: TaskInfo, visibleTasks: Int): TaskUpdate { + val screenBounds = taskInfo.configuration.windowConfiguration.bounds + val positionInParent = taskInfo.positionInParent + return TaskUpdate( + instanceId = taskInfo.taskId, + uid = taskInfo.effectiveUid, + taskHeight = screenBounds.height(), + taskWidth = screenBounds.width(), + taskX = positionInParent.x, + taskY = positionInParent.y, + visibleTaskCount = visibleTasks, + ) } /** Get [EnterReason] for this session enter */ - private fun getEnterReason(transitionInfo: TransitionInfo): EnterReason = - when { - transitionInfo.type == WindowManager.TRANSIT_WAKE -> EnterReason.SCREEN_ON + private fun getEnterReason(transitionInfo: TransitionInfo): EnterReason { + val enterReason = when { + transitionInfo.type == WindowManager.TRANSIT_WAKE + // If there is a screen lock, desktop window entry is after dismissing keyguard + || (transitionInfo.type == WindowManager.TRANSIT_TO_BACK + && wasPreviousTransitionExitByScreenOff) -> EnterReason.SCREEN_ON transitionInfo.type == Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP -> EnterReason.APP_HANDLE_DRAG transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON -> @@ -338,17 +375,23 @@ class DesktopModeLoggerTransitionObserver( else -> { ProtoLog.w( WM_SHELL_DESKTOP_MODE, - "Unknown enter reason for transition type ${transitionInfo.type}", + "Unknown enter reason for transition type: %s", transitionInfo.type ) EnterReason.UNKNOWN_ENTER } } + wasPreviousTransitionExitByScreenOff = false + return enterReason + } /** Get [ExitReason] for this session exit */ private fun getExitReason(transitionInfo: TransitionInfo): ExitReason = when { - transitionInfo.type == WindowManager.TRANSIT_SLEEP -> ExitReason.SCREEN_OFF + transitionInfo.type == WindowManager.TRANSIT_SLEEP -> { + wasPreviousTransitionExitByScreenOff = true + ExitReason.SCREEN_OFF + } transitionInfo.type == WindowManager.TRANSIT_CLOSE -> ExitReason.TASK_FINISHED transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG -> ExitReason.DRAG_TO_EXIT transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON -> @@ -359,7 +402,7 @@ class DesktopModeLoggerTransitionObserver( else -> { ProtoLog.w( WM_SHELL_DESKTOP_MODE, - "Unknown exit reason for transition type ${transitionInfo.type}", + "Unknown exit reason for transition type: %s", transitionInfo.type ) ExitReason.UNKNOWN_EXIT @@ -391,4 +434,12 @@ class DesktopModeLoggerTransitionObserver( return this.type == WindowManager.TRANSIT_TO_FRONT && this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS } + + companion object { + @VisibleForTesting + const val VISIBLE_TASKS_COUNTER_NAME = "desktop_mode_visible_tasks" + @VisibleForTesting + const val VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY = + "debug.tracing." + VISIBLE_TASKS_COUNTER_NAME + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt index 1a6ca0efa748..dba8c9367654 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt @@ -16,7 +16,7 @@ package com.android.wm.shell.desktopmode -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.UNKNOWN +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN import com.android.wm.shell.sysui.ShellCommandHandler import java.io.PrintWriter @@ -63,8 +63,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl pw.println("Error: task id should be an integer") return false } - - return controller.moveToDesktop(taskId, transitionSource = UNKNOWN) + return controller.moveTaskToDesktop(taskId, transitionSource = UNKNOWN) } private fun runMoveToNextDisplay(args: Array<String>, pw: PrintWriter): Boolean { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 7d01580ecb6e..759ed035895e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.desktopmode +import android.content.Context import android.graphics.Rect import android.graphics.Region import android.util.ArrayMap @@ -26,77 +27,139 @@ import android.window.WindowContainerToken import androidx.core.util.forEach import androidx.core.util.keyIterator import androidx.core.util.valueIterator +import com.android.internal.protolog.ProtoLog +import com.android.window.flags.Flags +import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository +import com.android.wm.shell.desktopmode.persistence.DesktopTask +import com.android.wm.shell.desktopmode.persistence.DesktopTaskState import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE -import com.android.wm.shell.util.KtProtoLog +import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.sysui.ShellInit import java.io.PrintWriter import java.util.concurrent.Executor import java.util.function.Consumer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch -/** Keeps track of task data related to desktop mode. */ -class DesktopModeTaskRepository { - - /** Task data that is tracked per display */ - private data class DisplayData( - /** - * Set of task ids that are marked as active in desktop mode. Active tasks in desktop mode - * are freeform tasks that are visible or have been visible after desktop mode was - * activated. Task gets removed from this list when it vanishes. Or when desktop mode is - * turned off. - */ +/** Tracks task data for Desktop Mode. */ +class DesktopModeTaskRepository ( + private val context: Context, + shellInit: ShellInit, + private val persistentRepository: DesktopPersistentRepository, + @ShellMainThread private val mainCoroutineScope: CoroutineScope, +){ + + /** + * Task data tracked per desktop. + * + * @property activeTasks task ids of active tasks currently or previously visible in Desktop + * mode session. Tasks become inactive when task closes or when desktop mode session ends. + * @property visibleTasks task ids for active freeform tasks that are currently visible. There + * might be other active tasks in desktop mode that are not visible. + * @property minimizedTasks task ids for active freeform tasks that are currently minimized. + * @property closingTasks task ids for tasks that are going to close, but are currently visible. + * @property freeformTasksInZOrder list of current freeform task ids ordered from top to bottom + * (top is at index 0). + */ + private data class DesktopTaskData( val activeTasks: ArraySet<Int> = ArraySet(), val visibleTasks: ArraySet<Int> = ArraySet(), val minimizedTasks: ArraySet<Int> = ArraySet(), - // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0). + // TODO(b/332682201): Remove when the repository state is updated via TransitionObserver + val closingTasks: ArraySet<Int> = ArraySet(), val freeformTasksInZOrder: ArrayList<Int> = ArrayList(), - ) + ) { + fun deepCopy(): DesktopTaskData = DesktopTaskData( + activeTasks = ArraySet(activeTasks), + visibleTasks = ArraySet(visibleTasks), + minimizedTasks = ArraySet(minimizedTasks), + closingTasks = ArraySet(closingTasks), + freeformTasksInZOrder = ArrayList(freeformTasksInZOrder) + ) + } - // Token of the current wallpaper activity, used to remove it when the last task is removed + /* Current wallpaper activity token to remove wallpaper activity when last task is removed. */ var wallpaperActivityToken: WindowContainerToken? = null + private val activeTasksListeners = ArraySet<ActiveTasksListener>() - // Track visible tasks separately because a task may be part of the desktop but not visible. private val visibleTasksListeners = ArrayMap<VisibleTasksListener, Executor>() - // Track corner/caption regions of desktop tasks, used to determine gesture exclusion + + /* Tracks corner/caption regions of desktop tasks, used to determine gesture exclusion. */ private val desktopExclusionRegions = SparseArray<Region>() - // Track last bounds of task before toggled to stable bounds + + /* Tracks last bounds of task before toggled to stable bounds. */ private val boundsBeforeMaximizeByTaskId = SparseArray<Rect>() + private var desktopGestureExclusionListener: Consumer<Region>? = null private var desktopGestureExclusionExecutor: Executor? = null - private val displayData = - object : SparseArray<DisplayData>() { - /** - * Get the [DisplayData] associated with this [displayId] - * - * Creates a new instance if one does not exist - */ - fun getOrCreate(displayId: Int): DisplayData { - if (!contains(displayId)) { - put(displayId, DisplayData()) + private val desktopTaskDataByDisplayId = object : SparseArray<DesktopTaskData>() { + /** Gets [DesktopTaskData] for existing [displayId] or creates a new one. */ + fun getOrCreate(displayId: Int): DesktopTaskData = + this[displayId] ?: DesktopTaskData().also { this[displayId] = it } + } + + init { + if (DesktopModeStatus.canEnterDesktopMode(context)) { + shellInit.addInitCallback(::initRepoFromPersistentStorage, this) + } + } + + private fun initRepoFromPersistentStorage() { + if (!Flags.enableDesktopWindowingPersistence()) return + // TODO: b/365962554 - Handle the case that user moves to desktop before it's initialized + mainCoroutineScope.launch { + val desktop = persistentRepository.readDesktop() + val maxTasks = + DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 } + ?: desktop.zOrderedTasksCount + + desktop.zOrderedTasksList + // Reverse it so we initialize the repo from bottom to top. + .reversed() + .map { taskId -> + desktop.tasksByTaskIdMap.getOrDefault( + taskId, + DesktopTask.getDefaultInstance() + ) + } + .filter { task -> task.desktopTaskState == DesktopTaskState.VISIBLE } + .take(maxTasks) + .forEach { task -> + addOrMoveFreeformTaskToTop(desktop.displayId, task.taskId) + addActiveTask(desktop.displayId, task.taskId) + updateTaskVisibility(desktop.displayId, task.taskId, visible = false) } - return get(displayId) - } } + } - /** Add a [ActiveTasksListener] to be notified of updates to active tasks in the repository. */ + /** Adds [activeTasksListener] to be notified of updates to active tasks. */ fun addActiveTaskListener(activeTasksListener: ActiveTasksListener) { activeTasksListeners.add(activeTasksListener) } - /** Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not. */ + /** Adds [visibleTasksListener] to be notified of updates to visible tasks. */ fun addVisibleTasksListener(visibleTasksListener: VisibleTasksListener, executor: Executor) { visibleTasksListeners[visibleTasksListener] = executor - displayData.keyIterator().forEach { displayId -> - val visibleTasksCount = getVisibleTaskCount(displayId) + desktopTaskDataByDisplayId.keyIterator().forEach { + val visibleTaskCount = getVisibleTaskCount(it) executor.execute { - visibleTasksListener.onTasksVisibilityChanged(displayId, visibleTasksCount) + visibleTasksListener.onTasksVisibilityChanged(it, visibleTaskCount) } } } - /** - * Add a Consumer which will inform other classes of changes to exclusion regions for all - * Desktop tasks. - */ + /** Updates tasks changes on all the active task listeners for given display id. */ + private fun updateActiveTasksListeners(displayId: Int) { + activeTasksListeners.onEach { it.onActiveTasksChanged(displayId) } + } + + /** Returns a list of all [DesktopTaskData] in the repository. */ + private fun desktopTaskDataSequence(): Sequence<DesktopTaskData> = + desktopTaskDataByDisplayId.valueIterator().asSequence() + + /** Adds [regionListener] to inform about changes to exclusion regions for all Desktop tasks. */ fun setExclusionRegionListener(regionListener: Consumer<Region>, executor: Executor) { desktopGestureExclusionListener = regionListener desktopGestureExclusionExecutor = executor @@ -105,7 +168,7 @@ class DesktopModeTaskRepository { } } - /** Create a new merged region representative of all exclusion regions in all desktop tasks. */ + /** Creates a new merged region representative of all exclusion regions in all desktop tasks. */ private fun calculateDesktopExclusionRegion(): Region { val desktopExclusionRegion = Region() desktopExclusionRegions.valueIterator().forEach { taskExclusionRegion -> @@ -114,169 +177,125 @@ class DesktopModeTaskRepository { return desktopExclusionRegion } - /** Remove a previously registered [ActiveTasksListener] */ + /** Remove the previously registered [activeTasksListener] */ fun removeActiveTasksListener(activeTasksListener: ActiveTasksListener) { activeTasksListeners.remove(activeTasksListener) } - /** Remove a previously registered [VisibleTasksListener] */ + /** Removes the previously registered [visibleTasksListener]. */ fun removeVisibleTasksListener(visibleTasksListener: VisibleTasksListener) { visibleTasksListeners.remove(visibleTasksListener) } - /** - * Mark a task with given [taskId] as active on given [displayId] - * - * @return `true` if the task was not active on given [displayId] - */ - fun addActiveTask(displayId: Int, taskId: Int): Boolean { - // Check if task is active on another display, if so, remove it - displayData.forEach { id, data -> - if (id != displayId && data.activeTasks.remove(taskId)) { - activeTasksListeners.onEach { it.onActiveTasksChanged(id) } - } - } + /** Adds task with [taskId] to the list of active tasks on [displayId]. */ + fun addActiveTask(displayId: Int, taskId: Int) { + // Removes task if it is active on another display excluding [displayId]. + removeActiveTask(taskId, excludedDisplayId = displayId) - val added = displayData.getOrCreate(displayId).activeTasks.add(taskId) - if (added) { - KtProtoLog.d( - WM_SHELL_DESKTOP_MODE, - "DesktopTaskRepo: add active task=%d displayId=%d", - taskId, - displayId - ) - activeTasksListeners.onEach { it.onActiveTasksChanged(displayId) } + if (desktopTaskDataByDisplayId.getOrCreate(displayId).activeTasks.add(taskId)) { + logD("Adds active task=%d displayId=%d", taskId, displayId) + updateActiveTasksListeners(displayId) } - return added } - /** - * Remove task with given [taskId] from active tasks. - * - * @return `true` if the task was active - */ - fun removeActiveTask(taskId: Int): Boolean { - var result = false - displayData.forEach { displayId, data -> - if (data.activeTasks.remove(taskId)) { - activeTasksListeners.onEach { it.onActiveTasksChanged(displayId) } - result = true + /** Removes task from active task list of displays excluding the [excludedDisplayId]. */ + fun removeActiveTask(taskId: Int, excludedDisplayId: Int? = null) { + desktopTaskDataByDisplayId.forEach { displayId, desktopTaskData -> + if ((displayId != excludedDisplayId) + && desktopTaskData.activeTasks.remove(taskId)) { + logD("Removed active task=%d displayId=%d", taskId, displayId) + updateActiveTasksListeners(displayId) } } - if (result) { - KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopTaskRepo: remove active task=%d", taskId) - } - return result } - /** Check if a task with the given [taskId] was marked as an active task */ - fun isActiveTask(taskId: Int): Boolean { - return displayData.valueIterator().asSequence().any { data -> - data.activeTasks.contains(taskId) + /** Adds given task to the closing task list for [displayId]. */ + fun addClosingTask(displayId: Int, taskId: Int) { + if (desktopTaskDataByDisplayId.getOrCreate(displayId).closingTasks.add(taskId)) { + logD("Added closing task=%d displayId=%d", taskId, displayId) + } else { + // If the task hasn't been removed from closing list after it disappeared. + logW("Task with taskId=%d displayId=%d is already closing", taskId, displayId) } } - /** Whether a task is visible. */ - fun isVisibleTask(taskId: Int): Boolean { - return displayData.valueIterator().asSequence().any { data -> - data.visibleTasks.contains(taskId) + /** Removes task from the list of closing tasks for [displayId]. */ + fun removeClosingTask(taskId: Int) { + desktopTaskDataByDisplayId.forEach { displayId, taskInfo -> + if (taskInfo.closingTasks.remove(taskId)) { + logD("Removed closing task=%d displayId=%d", taskId, displayId) + } } } - /** Return whether the given Task is minimized. */ - fun isMinimizedTask(taskId: Int): Boolean { - return displayData.valueIterator().asSequence().any { data -> - data.minimizedTasks.contains(taskId) + fun isActiveTask(taskId: Int) = desktopTaskDataSequence().any { taskId in it.activeTasks } + fun isClosingTask(taskId: Int) = desktopTaskDataSequence().any { taskId in it.closingTasks } + fun isVisibleTask(taskId: Int) = desktopTaskDataSequence().any { taskId in it.visibleTasks } + fun isMinimizedTask(taskId: Int) = desktopTaskDataSequence().any { taskId in it.minimizedTasks } + + /** Checks if a task is the only visible, non-closing, non-minimized task on its display. */ + fun isOnlyVisibleNonClosingTask(taskId: Int): Boolean = + desktopTaskDataSequence().any { it.visibleTasks + .subtract(it.closingTasks) + .subtract(it.minimizedTasks) + .singleOrNull() == taskId } - } - /** Check if a task with the given [taskId] is the only active task on its display */ - fun isOnlyActiveTask(taskId: Int): Boolean { - return displayData.valueIterator().asSequence().any { data -> - data.activeTasks.singleOrNull() == taskId - } - } + fun getActiveTasks(displayId: Int): ArraySet<Int> = + ArraySet(desktopTaskDataByDisplayId[displayId]?.activeTasks) - /** Get a set of the active tasks for given [displayId] */ - fun getActiveTasks(displayId: Int): ArraySet<Int> { - return ArraySet(displayData[displayId]?.activeTasks) - } + fun getMinimizedTasks(displayId: Int): ArraySet<Int> = + ArraySet(desktopTaskDataByDisplayId[displayId]?.minimizedTasks) - /** - * Returns whether Desktop Mode is currently showing any tasks, i.e. whether any Desktop Tasks - * are visible. - */ - fun isDesktopModeShowing(displayId: Int): Boolean = getVisibleTaskCount(displayId) > 0 + /** Returns all active non-minimized tasks for [displayId] ordered from top to bottom. */ + fun getActiveNonMinimizedOrderedTasks(displayId: Int): List<Int> = + getFreeformTasksInZOrder(displayId).filter { !isMinimizedTask(it) } - /** - * Returns a list of Tasks IDs representing all active non-minimized Tasks on the given display, - * ordered from front to back. - */ - fun getActiveNonMinimizedTasksOrderedFrontToBack(displayId: Int): List<Int> { - val activeTasks = getActiveTasks(displayId) - val allTasksInZOrder = getFreeformTasksInZOrder(displayId) - return activeTasks - // Don't show already minimized Tasks - .filter { taskId -> !isMinimizedTask(taskId) } - .sortedBy { taskId -> allTasksInZOrder.indexOf(taskId) } + /** Returns the count of active non-minimized tasks for [displayId]. */ + fun getActiveNonMinimizedTaskCount(displayId: Int): Int { + return getActiveTasks(displayId).count { !isMinimizedTask(it) } } - /** Get a list of freeform tasks, ordered from top-bottom (top at index 0). */ + /** Returns a list of freeform tasks, ordered from top-bottom (top at index 0). */ fun getFreeformTasksInZOrder(displayId: Int): ArrayList<Int> = - ArrayList(displayData[displayId]?.freeformTasksInZOrder ?: emptyList()) + ArrayList(desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder ?: emptyList()) + + /** Removes task from visible tasks of all displays except [excludedDisplayId]. */ + private fun removeVisibleTask(taskId: Int, excludedDisplayId: Int? = null) { + desktopTaskDataByDisplayId.forEach { displayId, data -> + if ((displayId != excludedDisplayId) && data.visibleTasks.remove(taskId)) { + notifyVisibleTaskListeners(displayId, data.visibleTasks.size) + } + } + } /** - * Updates whether a freeform task with this id is visible or not and notifies listeners. + * Updates visibility of a freeform task with [taskId] on [displayId] and notifies listeners. * - * If the task was visible on a different display with a different displayId, it is removed from - * the set of visible tasks on that display. Listeners will be notified. + * If task was visible on a different display with a different [displayId], removes from + * the set of visible tasks on that display and notifies listeners. */ - fun updateVisibleFreeformTasks(displayId: Int, taskId: Int, visible: Boolean) { + fun updateTaskVisibility(displayId: Int, taskId: Int, visible: Boolean) { if (visible) { - // Task is visible. Check if we need to remove it from any other display. - val otherDisplays = displayData.keyIterator().asSequence().filter { it != displayId } - for (otherDisplayId in otherDisplays) { - if (displayData[otherDisplayId].visibleTasks.remove(taskId)) { - notifyVisibleTaskListeners( - otherDisplayId, - displayData[otherDisplayId].visibleTasks.size - ) - } - } + // If task is visible, remove it from any other display besides [displayId]. + removeVisibleTask(taskId, excludedDisplayId = displayId) } else if (displayId == INVALID_DISPLAY) { // Task has vanished. Check which display to remove the task from. - displayData.forEach { displayId, data -> - if (data.visibleTasks.remove(taskId)) { - notifyVisibleTaskListeners(displayId, data.visibleTasks.size) - } - } + removeVisibleTask(taskId) return } - val prevCount = getVisibleTaskCount(displayId) if (visible) { - displayData.getOrCreate(displayId).visibleTasks.add(taskId) + desktopTaskDataByDisplayId.getOrCreate(displayId).visibleTasks.add(taskId) unminimizeTask(displayId, taskId) } else { - displayData[displayId]?.visibleTasks?.remove(taskId) + desktopTaskDataByDisplayId[displayId]?.visibleTasks?.remove(taskId) } val newCount = getVisibleTaskCount(displayId) - - // Check if count changed if (prevCount != newCount) { - KtProtoLog.d( - WM_SHELL_DESKTOP_MODE, - "DesktopTaskRepo: update task visibility taskId=%d visible=%b displayId=%d", - taskId, - visible, - displayId - ) - KtProtoLog.d( - WM_SHELL_DESKTOP_MODE, - "DesktopTaskRepo: visibleTaskCount has changed from %d to %d", - prevCount, - newCount - ) + logD("Update task visibility taskId=%d visible=%b displayId=%d", + taskId, visible, displayId) + logD("VisibleTaskCount has changed from %d to %d", prevCount, newCount) notifyVisibleTaskListeners(displayId, newCount) } } @@ -287,72 +306,89 @@ class DesktopModeTaskRepository { } } - /** Get number of tasks that are marked as visible on given [displayId] */ - fun getVisibleTaskCount(displayId: Int): Int { - KtProtoLog.d( - WM_SHELL_DESKTOP_MODE, - "DesktopTaskRepo: visibleTaskCount= %d", - displayData[displayId]?.visibleTasks?.size ?: 0 - ) - return displayData[displayId]?.visibleTasks?.size ?: 0 - } + /** Gets number of visible tasks on given [displayId] */ + fun getVisibleTaskCount(displayId: Int): Int = + desktopTaskDataByDisplayId[displayId]?.visibleTasks?.size ?: 0.also { + logD("getVisibleTaskCount=$it") + } - /** Add (or move if it already exists) the task to the top of the ordered list. */ - // TODO(b/342417921): Identify if there is additional checks needed to move tasks for - // multi-display scenarios. + /** + * Adds task (or moves if it already exists) to the top of the ordered list. + * + * Unminimizes the task if it is minimized. + */ fun addOrMoveFreeformTaskToTop(displayId: Int, taskId: Int) { - KtProtoLog.d( - WM_SHELL_DESKTOP_MODE, - "DesktopTaskRepo: add or move task to top: display=%d, taskId=%d", - displayId, - taskId - ) - displayData[displayId]?.freeformTasksInZOrder?.remove(taskId) - displayData.getOrCreate(displayId).freeformTasksInZOrder.add(0, taskId) + logD("Add or move task to top: display=%d taskId=%d", taskId, displayId) + desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.remove(taskId) + desktopTaskDataByDisplayId.getOrCreate(displayId).freeformTasksInZOrder.add(0, taskId) + // Unminimize the task if it is minimized. + unminimizeTask(displayId, taskId) + if (Flags.enableDesktopWindowingPersistence()) { + updatePersistentRepository(displayId) + } } - /** Mark a Task as minimized. */ + /** Minimizes the task for [taskId] and [displayId] */ fun minimizeTask(displayId: Int, taskId: Int) { - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopModeTaskRepository: minimize Task: display=%d, task=%d", - displayId, - taskId - ) - displayData.getOrCreate(displayId).minimizedTasks.add(taskId) + logD("Minimize Task: display=%d, task=%d", displayId, taskId) + desktopTaskDataByDisplayId.getOrCreate(displayId).minimizedTasks.add(taskId) + if (Flags.enableDesktopWindowingPersistence()) { + updatePersistentRepository(displayId) + } } - /** Mark a Task as non-minimized. */ + /** Unminimizes the task for [taskId] and [displayId] */ fun unminimizeTask(displayId: Int, taskId: Int) { - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopModeTaskRepository: unminimize Task: display=%d, task=%d", - displayId, - taskId - ) - displayData[displayId]?.minimizedTasks?.remove(taskId) + logD("Unminimize Task: display=%d, task=%d", displayId, taskId) + desktopTaskDataByDisplayId[displayId]?.minimizedTasks?.remove(taskId) ?: + logW("Unminimize Task: display=%d, task=%d, no task data", displayId, taskId) } - /** Remove the task from the ordered list. */ + private fun getDisplayIdForTask(taskId: Int): Int? { + desktopTaskDataByDisplayId.forEach { displayId, data -> + if (taskId in data.freeformTasksInZOrder) { + return displayId + } + } + logW("No display id found for task: taskId=%d", taskId) + return null + } + + /** + * Removes [taskId] from the respective display. If [INVALID_DISPLAY], the original display id + * will be looked up from the task id. + */ fun removeFreeformTask(displayId: Int, taskId: Int) { - KtProtoLog.d( - WM_SHELL_DESKTOP_MODE, - "DesktopTaskRepo: remove freeform task from ordered list: display=%d, taskId=%d", - displayId, - taskId - ) - displayData[displayId]?.freeformTasksInZOrder?.remove(taskId) + logD("Removes freeform task: taskId=%d", taskId) + if (displayId == INVALID_DISPLAY) { + // Removes the original display id of the task. + getDisplayIdForTask(taskId)?.let { removeTaskFromDisplay(it, taskId) } + } else { + removeTaskFromDisplay(displayId, taskId) + } + } + + /** Removes given task from a valid [displayId] and updates the repository state. */ + private fun removeTaskFromDisplay(displayId: Int, taskId: Int) { + logD("Removes freeform task: taskId=%d, displayId=%d", taskId, displayId) + desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.remove(taskId) boundsBeforeMaximizeByTaskId.remove(taskId) - KtProtoLog.d( - WM_SHELL_DESKTOP_MODE, - "DesktopTaskRepo: remaining freeform tasks: %s", - displayData[displayId]?.freeformTasksInZOrder?.toDumpString() ?: "" - ) + logD("Remaining freeform tasks: %s", + desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.toDumpString()) + // Remove task from unminimized task if it is minimized. + unminimizeTask(displayId, taskId) + removeActiveTask(taskId) + updateTaskVisibility(displayId, taskId, visible = false) + if (Flags.enableDesktopWindowingPersistence()) { + updatePersistentRepository(displayId) + } } /** - * Updates the active desktop gesture exclusion regions; if desktopExclusionRegions has been - * accepted by desktopGestureExclusionListener, it will be updated in the appropriate classes. + * Updates active desktop gesture exclusion regions. + * + * If [desktopExclusionRegions] is accepted by [desktopGestureExclusionListener], updates it in + * appropriate classes. */ fun updateTaskExclusionRegions(taskId: Int, taskExclusionRegions: Region) { desktopExclusionRegions.put(taskId, taskExclusionRegions) @@ -362,9 +398,10 @@ class DesktopModeTaskRepository { } /** - * Removes the desktop gesture exclusion region for the specified task; if exclusionRegion has - * been accepted by desktopGestureExclusionListener, it will be updated in the appropriate - * classes. + * Removes desktop gesture exclusion region for the specified task. + * + * If [desktopExclusionRegions] is accepted by [desktopGestureExclusionListener], updates it in + * appropriate classes. */ fun removeExclusionRegion(taskId: Int) { desktopExclusionRegions.delete(taskId) @@ -374,26 +411,45 @@ class DesktopModeTaskRepository { } /** Removes and returns the bounds saved before maximizing the given task. */ - fun removeBoundsBeforeMaximize(taskId: Int): Rect? { - return boundsBeforeMaximizeByTaskId.removeReturnOld(taskId) - } + fun removeBoundsBeforeMaximize(taskId: Int): Rect? = + boundsBeforeMaximizeByTaskId.removeReturnOld(taskId) /** Saves the bounds of the given task before maximizing. */ - fun saveBoundsBeforeMaximize(taskId: Int, bounds: Rect) { + fun saveBoundsBeforeMaximize(taskId: Int, bounds: Rect) = boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds)) + + private fun updatePersistentRepository(displayId: Int) { + // Create a deep copy of the data + desktopTaskDataByDisplayId[displayId]?.deepCopy()?.let { desktopTaskDataByDisplayIdCopy -> + mainCoroutineScope.launch { + try { + persistentRepository.addOrUpdateDesktop( + visibleTasks = desktopTaskDataByDisplayIdCopy.visibleTasks, + minimizedTasks = desktopTaskDataByDisplayIdCopy.minimizedTasks, + freeformTasksInZOrder = desktopTaskDataByDisplayIdCopy.freeformTasksInZOrder + ) + } catch (exception: Exception) { + logE( + "An exception occurred while updating the persistent repository \n%s", + exception.stackTrace + ) + } + } + } } + internal fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("${prefix}DesktopModeTaskRepository") - dumpDisplayData(pw, innerPrefix) + dumpDesktopTaskData(pw, innerPrefix) pw.println("${innerPrefix}activeTasksListeners=${activeTasksListeners.size}") pw.println("${innerPrefix}visibleTasksListeners=${visibleTasksListeners.size}") } - private fun dumpDisplayData(pw: PrintWriter, prefix: String) { + private fun dumpDesktopTaskData(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " - displayData.forEach { displayId, data -> + desktopTaskDataByDisplayId.forEach { displayId, data -> pw.println("${prefix}Display $displayId:") pw.println("${innerPrefix}activeTasks=${data.activeTasks.toDumpString()}") pw.println("${innerPrefix}visibleTasks=${data.visibleTasks.toDumpString()}") @@ -403,23 +459,33 @@ class DesktopModeTaskRepository { } } - /** - * Defines interface for classes that can listen to changes for active tasks in desktop mode. - */ + /** Listens to changes for active tasks in desktop mode. */ interface ActiveTasksListener { - /** Called when the active tasks change in desktop mode. */ fun onActiveTasksChanged(displayId: Int) {} } - /** - * Defines interface for classes that can listen to changes for visible tasks in desktop mode. - */ + /** Listens to changes for visible tasks in desktop mode. */ interface VisibleTasksListener { - /** Called when the desktop changes the number of visible freeform tasks. */ fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {} } -} -private fun <T> Iterable<T>.toDumpString(): String { - return joinToString(separator = ", ", prefix = "[", postfix = "]") + private fun logD(msg: String, vararg arguments: Any?) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + private fun logW(msg: String, vararg arguments: Any?) { + ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + private fun logE(msg: String, vararg arguments: Any?) { + ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + companion object { + private const val TAG = "DesktopModeTaskRepository" + } } + +private fun <T> Iterable<T>.toDumpString(): String = + joinToString(separator = ", ", prefix = "[", postfix = "]") + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypes.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypes.kt index b24bd10eaa0d..d6fccd116061 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypes.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypes.kt @@ -17,7 +17,7 @@ package com.android.wm.shell.desktopmode import android.view.WindowManager.TransitionType -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_TYPES /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt index 217b1d356122..bd6172226cf2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt @@ -19,6 +19,8 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RunningTaskInfo +import android.app.TaskInfo +import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED import android.content.pm.ActivityInfo.isFixedOrientationLandscape import android.content.pm.ActivityInfo.isFixedOrientationPortrait import android.content.res.Configuration.ORIENTATION_LANDSCAPE @@ -50,46 +52,65 @@ fun calculateInitialBounds( val idealSize = calculateIdealSize(screenBounds, scale) // If no top activity exists, apps fullscreen bounds and aspect ratio cannot be calculated. // Instead default to the desired initial bounds. + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + if (hasFullscreenOverride(taskInfo)) { + // If the activity has a fullscreen override applied, it should be treated as + // resizeable and match the device orientation. Thus the ideal size can be + // applied. + return positionInScreen(idealSize, stableBounds) + } val topActivityInfo = - taskInfo.topActivityInfo ?: return positionInScreen(idealSize, screenBounds) + taskInfo.topActivityInfo ?: return positionInScreen(idealSize, stableBounds) val initialSize: Size = when (taskInfo.configuration.orientation) { ORIENTATION_LANDSCAPE -> { - if (taskInfo.isResizeable) { + if (taskInfo.canChangeAspectRatio) { if (isFixedOrientationPortrait(topActivityInfo.screenOrientation)) { - // Respect apps fullscreen width - Size(taskInfo.appCompatTaskInfo.topActivityLetterboxWidth, idealSize.height) + // For portrait resizeable activities, respect apps fullscreen width but + // apply ideal size height. + Size(taskInfo.appCompatTaskInfo.topActivityLetterboxAppWidth, + idealSize.height) } else { + // For landscape resizeable activities, simply apply ideal size. idealSize } } else { - maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio) + // If activity is unresizeable, regardless of orientation, calculate maximum + // size (within the ideal size) maintaining original aspect ratio. + maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio) } } ORIENTATION_PORTRAIT -> { val customPortraitWidthForLandscapeApp = screenBounds.width() - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2) - if (taskInfo.isResizeable) { + if (taskInfo.canChangeAspectRatio) { if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) { - // Respect apps fullscreen height and apply custom app width + // For landscape resizeable activities, respect apps fullscreen height and + // apply custom app width. Size( customPortraitWidthForLandscapeApp, - taskInfo.appCompatTaskInfo.topActivityLetterboxHeight + taskInfo.appCompatTaskInfo.topActivityLetterboxAppHeight ) } else { + // For portrait resizeable activities, simply apply ideal size. idealSize } } else { if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) { - // Apply custom app width and calculate maximum size - maximumSizeMaintainingAspectRatio( + // For landscape unresizeable activities, apply custom app width to ideal + // size and calculate maximum size with this area while maintaining original + // aspect ratio. + maximizeSizeGivenAspectRatio( taskInfo, Size(customPortraitWidthForLandscapeApp, idealSize.height), appAspectRatio ) } else { - maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio) + // For portrait unresizeable activities, calculate maximum size (within the + // ideal size) maintaining original aspect ratio. + maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio) } } } @@ -98,14 +119,14 @@ fun calculateInitialBounds( } } - return positionInScreen(initialSize, screenBounds) + return positionInScreen(initialSize, stableBounds) } /** * Calculates the largest size that can fit in a given area while maintaining a specific aspect * ratio. */ -private fun maximumSizeMaintainingAspectRatio( +fun maximizeSizeGivenAspectRatio( taskInfo: RunningTaskInfo, targetArea: Size, aspectRatio: Float @@ -114,7 +135,8 @@ private fun maximumSizeMaintainingAspectRatio( val targetWidth = targetArea.width val finalHeight: Int val finalWidth: Int - if (isFixedOrientationPortrait(taskInfo.topActivityInfo!!.screenOrientation)) { + // Get orientation either through top activity or task's orientation + if (taskInfo.hasPortraitTopActivity()) { val tempWidth = (targetHeight / aspectRatio).toInt() if (tempWidth <= targetWidth) { finalHeight = targetHeight @@ -137,10 +159,10 @@ private fun maximumSizeMaintainingAspectRatio( } /** Calculates the aspect ratio of an activity from its fullscreen bounds. */ -private fun calculateAspectRatio(taskInfo: RunningTaskInfo): Float { - if (taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed) { - val appLetterboxWidth = taskInfo.appCompatTaskInfo.topActivityLetterboxWidth - val appLetterboxHeight = taskInfo.appCompatTaskInfo.topActivityLetterboxHeight +fun calculateAspectRatio(taskInfo: RunningTaskInfo): Float { + val appLetterboxWidth = taskInfo.appCompatTaskInfo.topActivityLetterboxAppWidth + val appLetterboxHeight = taskInfo.appCompatTaskInfo.topActivityLetterboxAppHeight + if (taskInfo.appCompatTaskInfo.isTopActivityLetterboxed || !taskInfo.canChangeAspectRatio) { return maxOf(appLetterboxWidth, appLetterboxHeight) / minOf(appLetterboxWidth, appLetterboxHeight).toFloat() } @@ -149,6 +171,18 @@ private fun calculateAspectRatio(taskInfo: RunningTaskInfo): Float { minOf(appBounds.height(), appBounds.width()).toFloat() } +/** Returns true if task's width or height is maximized else returns false. */ +fun isTaskWidthOrHeightEqual(taskBounds: Rect, stableBounds: Rect): Boolean { + return taskBounds.width() == stableBounds.width() || + taskBounds.height() == stableBounds.height() +} + +/** Returns true if task bound is equal to stable bounds else returns false. */ +fun isTaskBoundsEqual(taskBounds: Rect, stableBounds: Rect): Boolean { + return taskBounds.width() == stableBounds.width() && + taskBounds.height() == stableBounds.height() +} + /** * Calculates the desired initial bounds for applications in desktop windowing. This is done as a * scale of the screen bounds. @@ -160,14 +194,58 @@ private fun calculateIdealSize(screenBounds: Rect, scale: Float): Size { } /** Adjusts bounds to be positioned in the middle of the screen. */ -private fun positionInScreen(desiredSize: Size, screenBounds: Rect): Rect { - // TODO(b/325240051): Position apps with bottom heavy offset - val heightOffset = (screenBounds.height() - desiredSize.height) / 2 - val widthOffset = (screenBounds.width() - desiredSize.width) / 2 - return Rect( - widthOffset, - heightOffset, - desiredSize.width + widthOffset, - desiredSize.height + heightOffset - ) +private fun positionInScreen(desiredSize: Size, stableBounds: Rect): Rect = + Rect(0, 0, desiredSize.width, desiredSize.height).apply { + val offset = DesktopTaskPosition.Center.getTopLeftCoordinates(stableBounds, this) + offsetTo(offset.x, offset.y) + } + +/** + * Whether the activity's aspect ratio can be changed or if it should be maintained as if it was + * unresizeable. + */ +private val TaskInfo.canChangeAspectRatio: Boolean + get() = isResizeable && !appCompatTaskInfo.hasMinAspectRatioOverride() + +/** + * Adjusts bounds to be positioned in the middle of the area provided, not necessarily the + * entire screen, as area can be offset by left and top start. + */ +fun centerInArea(desiredSize: Size, areaBounds: Rect, leftStart: Int, topStart: Int): Rect { + val heightOffset = (areaBounds.height() - desiredSize.height) / 2 + val widthOffset = (areaBounds.width() - desiredSize.width) / 2 + + val newLeft = leftStart + widthOffset + val newTop = topStart + heightOffset + val newRight = newLeft + desiredSize.width + val newBottom = newTop + desiredSize.height + + return Rect(newLeft, newTop, newRight, newBottom) +} + +private fun TaskInfo.hasPortraitTopActivity(): Boolean { + val topActivityScreenOrientation = + topActivityInfo?.screenOrientation ?: SCREEN_ORIENTATION_UNSPECIFIED + val appBounds = configuration.windowConfiguration.appBounds + + return when { + // First check if activity has portrait screen orientation + topActivityScreenOrientation != SCREEN_ORIENTATION_UNSPECIFIED -> { + isFixedOrientationPortrait(topActivityScreenOrientation) + } + + // Then check if the activity is portrait when letterboxed + appCompatTaskInfo.isTopActivityLetterboxed -> appCompatTaskInfo.isTopActivityPillarboxed + + // Then check if the activity is portrait + appBounds != null -> appBounds.height() > appBounds.width() + + // Otherwise just take the orientation of the task + else -> isFixedOrientationPortrait(configuration.orientation) + } +} + +private fun hasFullscreenOverride(taskInfo: RunningTaskInfo): Boolean { + return taskInfo.appCompatTaskInfo.isUserFullscreenOverrideEnabled + || taskInfo.appCompatTaskInfo.isSystemFullscreenOverrideEnabled } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index ed0d2b87b03f..72619195fb3f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -16,7 +16,6 @@ package com.android.wm.shell.desktopmode; -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; @@ -27,8 +26,8 @@ import android.animation.AnimatorListenerAdapter; import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; -import android.app.WindowConfiguration; import android.content.Context; import android.content.res.Resources; import android.graphics.PixelFormat; @@ -46,6 +45,7 @@ import android.view.animation.DecelerateInterpolator; import androidx.annotation.VisibleForTesting; +import com.android.internal.policy.SystemBarUtils; import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.DisplayController; @@ -69,6 +69,37 @@ public class DesktopModeVisualIndicator { TO_SPLIT_RIGHT_INDICATOR } + /** + * The conditions surrounding the drag event that led to the indicator's creation. + */ + public enum DragStartState { + /** The indicator is resulting from a freeform task drag. */ + FROM_FREEFORM, + /** The indicator is resulting from a split screen task drag */ + FROM_SPLIT, + /** The indicator is resulting from a fullscreen task drag */ + FROM_FULLSCREEN, + /** The indicator is resulting from an Intent generated during a drag-and-drop event */ + DRAGGED_INTENT; + + /** + * Get the {@link DragStartState} of a drag event based on the windowing mode of the task. + * Note that DRAGGED_INTENT will be specified by the caller if needed and not returned + * here. + */ + public static DesktopModeVisualIndicator.DragStartState getDragStartState( + ActivityManager.RunningTaskInfo taskInfo + ) { + if (taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { + return FROM_FULLSCREEN; + } else if (taskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { + return FROM_SPLIT; + } else if (taskInfo.isFreeform()) { + return FROM_FREEFORM; + } else return null; + } + } + private final Context mContext; private final DisplayController mDisplayController; private final RootTaskDisplayAreaOrganizer mRootTdaOrganizer; @@ -81,11 +112,13 @@ public class DesktopModeVisualIndicator { private View mView; private IndicatorType mCurrentType; + private DragStartState mDragStartState; public DesktopModeVisualIndicator(SyncTransactionQueue syncQueue, ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController, Context context, SurfaceControl taskSurface, - RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer) { + RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer, + DragStartState dragStartState) { mSyncQueue = syncQueue; mTaskInfo = taskInfo; mDisplayController = displayController; @@ -93,6 +126,7 @@ public class DesktopModeVisualIndicator { mTaskSurface = taskSurface; mRootTdaOrganizer = taskDisplayAreaOrganizer; mCurrentType = IndicatorType.NO_INDICATOR; + mDragStartState = dragStartState; } /** @@ -100,24 +134,23 @@ public class DesktopModeVisualIndicator { * display, including no visible indicator. */ @NonNull - IndicatorType updateIndicatorType(PointF inputCoordinates, int windowingMode) { + IndicatorType updateIndicatorType(PointF inputCoordinates) { final DisplayLayout layout = mDisplayController.getDisplayLayout(mTaskInfo.displayId); // If we are in freeform, we don't want a visible indicator in the "freeform" drag zone. IndicatorType result = IndicatorType.NO_INDICATOR; final int transitionAreaWidth = mContext.getResources().getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_transition_area_width); + com.android.wm.shell.R.dimen.desktop_mode_transition_region_thickness); // Because drags in freeform use task position for indicator calculation, we need to // account for the possibility of the task going off the top of the screen by captionHeight final int captionHeight = mContext.getResources().getDimensionPixelSize( com.android.wm.shell.R.dimen.desktop_mode_freeform_decor_caption_height); - final Region fullscreenRegion = calculateFullscreenRegion(layout, windowingMode, + final Region fullscreenRegion = calculateFullscreenRegion(layout, captionHeight); + final Region splitLeftRegion = calculateSplitLeftRegion(layout, transitionAreaWidth, + captionHeight); + final Region splitRightRegion = calculateSplitRightRegion(layout, transitionAreaWidth, captionHeight); - final Region splitLeftRegion = calculateSplitLeftRegion(layout, windowingMode, - transitionAreaWidth, captionHeight); - final Region splitRightRegion = calculateSplitRightRegion(layout, windowingMode, - transitionAreaWidth, captionHeight); - final Region toDesktopRegion = calculateToDesktopRegion(layout, windowingMode, - splitLeftRegion, splitRightRegion, fullscreenRegion); + final Region toDesktopRegion = calculateToDesktopRegion(layout, splitLeftRegion, + splitRightRegion, fullscreenRegion); if (fullscreenRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) { result = IndicatorType.TO_FULLSCREEN_INDICATOR; } @@ -130,30 +163,34 @@ public class DesktopModeVisualIndicator { if (toDesktopRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) { result = IndicatorType.TO_DESKTOP_INDICATOR; } - transitionIndicator(result); + if (mDragStartState != DragStartState.DRAGGED_INTENT) { + transitionIndicator(result); + } return result; } @VisibleForTesting - Region calculateFullscreenRegion(DisplayLayout layout, - @WindowConfiguration.WindowingMode int windowingMode, int captionHeight) { + Region calculateFullscreenRegion(DisplayLayout layout, int captionHeight) { final Region region = new Region(); - int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM - ? mContext.getResources().getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height) + int transitionHeight = mDragStartState == DragStartState.FROM_FREEFORM + || mDragStartState == DragStartState.DRAGGED_INTENT + ? SystemBarUtils.getStatusBarHeight(mContext) : 2 * layout.stableInsets().top; - // A thin, short Rect at the top of the screen. - if (windowingMode == WINDOWING_MODE_FREEFORM) { - int fromFreeformWidth = mContext.getResources().getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_width); - region.union(new Rect((layout.width() / 2) - (fromFreeformWidth / 2), + // A Rect at the top of the screen that takes up the center 40%. + if (mDragStartState == DragStartState.FROM_FREEFORM) { + final float toFullscreenScale = mContext.getResources().getFloat( + R.dimen.desktop_mode_fullscreen_region_scale); + final float toFullscreenWidth = (layout.width() * toFullscreenScale); + region.union(new Rect((int) ((layout.width() / 2f) - (toFullscreenWidth / 2f)), -captionHeight, - (layout.width() / 2) + (fromFreeformWidth / 2), + (int) ((layout.width() / 2f) + (toFullscreenWidth / 2f)), transitionHeight)); } - // A screen-wide, shorter Rect if the task is in fullscreen or split. - if (windowingMode == WINDOWING_MODE_FULLSCREEN - || windowingMode == WINDOWING_MODE_MULTI_WINDOW) { + // A screen-wide Rect if the task is in fullscreen, split, or a dragged intent. + if (mDragStartState == DragStartState.FROM_FULLSCREEN + || mDragStartState == DragStartState.FROM_SPLIT + || mDragStartState == DragStartState.DRAGGED_INTENT + ) { region.union(new Rect(0, -captionHeight, layout.width(), @@ -164,12 +201,11 @@ public class DesktopModeVisualIndicator { @VisibleForTesting Region calculateToDesktopRegion(DisplayLayout layout, - @WindowConfiguration.WindowingMode int windowingMode, Region splitLeftRegion, Region splitRightRegion, Region toFullscreenRegion) { final Region region = new Region(); // If in desktop, we need no region. Otherwise it's the same for all windowing modes. - if (windowingMode != WINDOWING_MODE_FREEFORM) { + if (mDragStartState != DragStartState.FROM_FREEFORM) { region.union(new Rect(0, 0, layout.width(), layout.height())); region.op(splitLeftRegion, Region.Op.DIFFERENCE); region.op(splitRightRegion, Region.Op.DIFFERENCE); @@ -180,11 +216,10 @@ public class DesktopModeVisualIndicator { @VisibleForTesting Region calculateSplitLeftRegion(DisplayLayout layout, - @WindowConfiguration.WindowingMode int windowingMode, int transitionEdgeWidth, int captionHeight) { final Region region = new Region(); // In freeform, keep the top corners clear. - int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM + int transitionHeight = mDragStartState == DragStartState.FROM_FREEFORM ? mContext.getResources().getDimensionPixelSize( com.android.wm.shell.R.dimen.desktop_mode_split_from_desktop_height) : -captionHeight; @@ -194,11 +229,10 @@ public class DesktopModeVisualIndicator { @VisibleForTesting Region calculateSplitRightRegion(DisplayLayout layout, - @WindowConfiguration.WindowingMode int windowingMode, int transitionEdgeWidth, int captionHeight) { final Region region = new Region(); // In freeform, keep the top corners clear. - int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM + int transitionHeight = mDragStartState == DragStartState.FROM_FREEFORM ? mContext.getResources().getDimensionPixelSize( com.android.wm.shell.R.dimen.desktop_mode_split_from_desktop_height) : -captionHeight; @@ -261,12 +295,22 @@ public class DesktopModeVisualIndicator { /** * Fade out indicator without fully releasing it. Animator fades it out while shrinking bounds. + * + * @param finishCallback called when animation ends or gets cancelled */ - private void fadeOutIndicator() { + void fadeOutIndicator(@Nullable Runnable finishCallback) { final VisualIndicatorAnimator animator = VisualIndicatorAnimator .fadeBoundsOut(mView, mCurrentType, mDisplayController.getDisplayLayout(mTaskInfo.displayId)); animator.start(); + if (finishCallback != null) { + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finishCallback.run(); + } + }); + } mCurrentType = IndicatorType.NO_INDICATOR; } @@ -281,7 +325,7 @@ public class DesktopModeVisualIndicator { if (mCurrentType == IndicatorType.NO_INDICATOR) { fadeInIndicator(newType); } else if (newType == IndicatorType.NO_INDICATOR) { - fadeOutIndicator(); + fadeOutIndicator(null /* finishCallback */); } else { final VisualIndicatorAnimator animator = VisualIndicatorAnimator.animateIndicatorType( mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt new file mode 100644 index 000000000000..65f12cf4a196 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2024 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.desktopmode + +import android.app.TaskInfo +import android.content.res.Resources +import android.graphics.Point +import android.graphics.Rect +import android.view.Gravity +import com.android.internal.annotations.VisibleForTesting +import com.android.wm.shell.desktopmode.DesktopTaskPosition.BottomLeft +import com.android.wm.shell.desktopmode.DesktopTaskPosition.BottomRight +import com.android.wm.shell.desktopmode.DesktopTaskPosition.Center +import com.android.wm.shell.desktopmode.DesktopTaskPosition.TopLeft +import com.android.wm.shell.desktopmode.DesktopTaskPosition.TopRight +import com.android.wm.shell.R + +/** + * The position of a task window in desktop mode. + */ +sealed class DesktopTaskPosition { + data object Center : DesktopTaskPosition() { + private const val WINDOW_HEIGHT_PROPORTION = 0.375 + + override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point { + val x = (frame.width() - window.width()) / 2 + // Position with more margin at the bottom. + val y = (frame.height() - window.height()) * WINDOW_HEIGHT_PROPORTION + frame.top + return Point(x, y.toInt()) + } + + override fun next(): DesktopTaskPosition { + return BottomRight + } + } + + data object BottomRight : DesktopTaskPosition() { + override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point { + return Point(frame.right - window.width(), frame.bottom - window.height()) + } + + override fun next(): DesktopTaskPosition { + return TopLeft + } + } + + data object TopLeft : DesktopTaskPosition() { + override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point { + return Point(frame.left, frame.top) + } + + override fun next(): DesktopTaskPosition { + return BottomLeft + } + } + + data object BottomLeft : DesktopTaskPosition() { + override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point { + return Point(frame.left, frame.bottom - window.height()) + } + + override fun next(): DesktopTaskPosition { + return TopRight + } + } + + data object TopRight : DesktopTaskPosition() { + override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point { + return Point(frame.right - window.width(), frame.top) + } + + override fun next(): DesktopTaskPosition { + return Center + } + } + + /** + * Returns the top left coordinates for the window to be placed in the given + * DesktopTaskPosition in the frame. + */ + abstract fun getTopLeftCoordinates(frame: Rect, window: Rect): Point + + abstract fun next(): DesktopTaskPosition +} + +/** + * If the app has specified horizontal or vertical gravity layout, don't change the + * task position for cascading effect. + */ +fun canChangeTaskPosition(taskInfo: TaskInfo): Boolean { + taskInfo.topActivityInfo?.windowLayout?.let { + val horizontalGravityApplied = it.gravity.and(Gravity.HORIZONTAL_GRAVITY_MASK) + val verticalGravityApplied = it.gravity.and(Gravity.VERTICAL_GRAVITY_MASK) + return horizontalGravityApplied == 0 && verticalGravityApplied == 0 + } + return true +} + +/** + * Returns the current DesktopTaskPosition for a given window in the frame. + */ +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) +fun Rect.getDesktopTaskPosition(bounds: Rect): DesktopTaskPosition { + return when { + top == bounds.top && left == bounds.left && bottom != bounds.bottom -> TopLeft + top == bounds.top && right == bounds.right && bottom != bounds.bottom -> TopRight + bottom == bounds.bottom && left == bounds.left && top != bounds.top -> BottomLeft + bottom == bounds.bottom && right == bounds.right && top != bounds.top -> BottomRight + else -> Center + } +} + +internal fun cascadeWindow(res: Resources, frame: Rect, prev: Rect, dest: Rect) { + val candidateBounds = Rect(dest) + val lastPos = frame.getDesktopTaskPosition(prev) + var destCoord = Center.getTopLeftCoordinates(frame, candidateBounds) + candidateBounds.offsetTo(destCoord.x, destCoord.y) + // If the default center position is not free or if last focused window is not at the + // center, get the next cascading window position. + if (!prevBoundsMovedAboveThreshold(res, prev, candidateBounds) || Center != lastPos) { + val nextCascadingPos = lastPos.next() + destCoord = nextCascadingPos.getTopLeftCoordinates(frame, dest) + } + dest.offsetTo(destCoord.x, destCoord.y) +} + +internal fun prevBoundsMovedAboveThreshold(res: Resources, prev: Rect, newBounds: Rect): Boolean { + // This is the required minimum dp for a task to be touchable. + val moveThresholdPx = res.getDimensionPixelSize( + R.dimen.freeform_required_visible_empty_space_in_header) + val leftFar = newBounds.left - prev.left > moveThresholdPx + val topFar = newBounds.top - prev.top > moveThresholdPx + val rightFar = prev.right - newBounds.right > moveThresholdPx + val bottomFar = prev.bottom - newBounds.bottom > moveThresholdPx + + return leftFar || topFar || rightFar || bottomFar +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index c5111d68881d..968f40c3df5d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RunningTaskInfo import android.app.ActivityOptions +import android.app.KeyguardManager import android.app.PendingIntent import android.app.TaskInfo import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME @@ -33,14 +34,17 @@ import android.graphics.Point import android.graphics.PointF import android.graphics.Rect import android.graphics.Region +import android.os.Binder +import android.os.Handler import android.os.IBinder import android.os.SystemProperties +import android.util.Size import android.view.Display.DEFAULT_DISPLAY +import android.view.DragEvent import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_NONE import android.view.WindowManager.TRANSIT_OPEN -import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.RemoteTransition import android.window.TransitionInfo @@ -48,7 +52,12 @@ import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import androidx.annotation.BinderThread import com.android.internal.annotations.VisibleForTesting +import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD +import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE +import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE +import com.android.internal.jank.InteractionJankMonitor import com.android.internal.policy.ScreenDecorationsUtils +import com.android.internal.protolog.ProtoLog import com.android.window.flags.Flags import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer @@ -62,35 +71,43 @@ import com.android.wm.shell.common.RemoteCallable import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SingleInstanceRemoteListener import com.android.wm.shell.common.SyncTransactionQueue -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource -import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT -import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT -import com.android.wm.shell.compatui.isSingleTopActivityTranslucent +import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener +import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.DragStartState +import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.DragToDesktopStateListener import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.recents.RecentTasksController import com.android.wm.shell.recents.RecentsTransitionHandler import com.android.wm.shell.recents.RecentsTransitionStateListener -import com.android.wm.shell.shared.DesktopModeStatus -import com.android.wm.shell.shared.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE -import com.android.wm.shell.shared.DesktopModeStatus.useDesktopOverrideDensity +import com.android.wm.shell.shared.ShellSharedConstants +import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.annotations.ExternalThread import com.android.wm.shell.shared.annotations.ShellMainThread +import android.window.flags.DesktopModeFlags +import android.window.flags.DesktopModeFlags.DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE +import android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY +import android.window.flags.DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.useDesktopOverrideDensity +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DESKTOP_MODE import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit -import com.android.wm.shell.sysui.ShellSharedConstants import com.android.wm.shell.transition.OneShotRemoteHandler import com.android.wm.shell.transition.Transitions -import com.android.wm.shell.util.KtProtoLog import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility import com.android.wm.shell.windowdecor.MoveToDesktopAnimator +import com.android.wm.shell.windowdecor.OnTaskRepositionAnimationListener import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener import com.android.wm.shell.windowdecor.extension.isFullscreen +import com.android.wm.shell.windowdecor.extension.isMultiWindow import java.io.PrintWriter import java.util.Optional import java.util.concurrent.Executor @@ -108,18 +125,23 @@ class DesktopTasksController( private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, private val dragAndDropController: DragAndDropController, private val transitions: Transitions, + private val keyguardManager: KeyguardManager, + private val returnToDragStartAnimator: ReturnToDragStartAnimator, private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler, private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler, + private val desktopModeDragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler, private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler, private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler, - private val desktopModeTaskRepository: DesktopModeTaskRepository, + private val taskRepository: DesktopModeTaskRepository, private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver, private val launchAdjacentController: LaunchAdjacentController, private val recentsTransitionHandler: RecentsTransitionHandler, private val multiInstanceHelper: MultiInstanceHelper, @ShellMainThread private val mainExecutor: ShellExecutor, private val desktopTasksLimiter: Optional<DesktopTasksLimiter>, - private val recentTasksController: RecentTasksController? + private val recentTasksController: RecentTasksController?, + private val interactionJankMonitor: InteractionJankMonitor, + @ShellMainThread private val handler: Handler, ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler, @@ -134,12 +156,6 @@ class DesktopTasksController( visualIndicator?.releaseVisualIndicator(t) visualIndicator = null } - private val taskVisibilityListener = - object : VisibleTasksListener { - override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) { - launchAdjacentController.launchAdjacentEnabled = visibleTasksCount == 0 - } - } private val dragToDesktopStateListener = object : DragToDesktopStateListener { override fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction) { @@ -151,24 +167,15 @@ class DesktopTasksController( } private fun removeVisualIndicator(tx: SurfaceControl.Transaction) { - visualIndicator?.releaseVisualIndicator(tx) - visualIndicator = null + visualIndicator?.fadeOutIndicator { + visualIndicator?.releaseVisualIndicator(tx) + visualIndicator = null + } } } - private val sysUIPackageName = context.resources.getString( - com.android.internal.R.string.config_systemUi) - - private val transitionAreaHeight - get() = - context.resources.getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height - ) - private val transitionAreaWidth - get() = - context.resources.getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_transition_area_width - ) + @VisibleForTesting + var taskbarDesktopTaskListener: TaskbarDesktopTaskListener? = null /** Task id of the task currently being dragged from fullscreen/split. */ val draggingTaskId @@ -176,6 +183,9 @@ class DesktopTasksController( private var recentsAnimationRunning = false private lateinit var splitScreenController: SplitScreenController + // Launch cookie used to identify a drag and drop transition to fullscreen after it has begun. + // Used to prevent handleRequest from moving the new fullscreen task to freeform. + private var dragAndDropFullscreenCookie: Binder? = null init { desktopMode = DesktopModeImpl() @@ -185,7 +195,7 @@ class DesktopTasksController( } private fun onInit() { - KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController") + logD("onInit") shellCommandHandler.addDumpCallback(this::dump, this) shellCommandHandler.addCommandCallback("desktopmode", desktopModeShellCommandHandler, this) shellController.addExternalInterface( @@ -194,16 +204,11 @@ class DesktopTasksController( this ) transitions.addHandler(this) - desktopModeTaskRepository.addVisibleTasksListener(taskVisibilityListener, mainExecutor) - dragToDesktopTransitionHandler.setDragToDesktopStateListener(dragToDesktopStateListener) + dragToDesktopTransitionHandler.dragToDesktopStateListener = dragToDesktopStateListener recentsTransitionHandler.addTransitionStateListener( object : RecentsTransitionStateListener { override fun onAnimationStateChanged(running: Boolean) { - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: recents animation state changed running=%b", - running - ) + logV("Recents animation state changed running=%b", running) recentsAnimationRunning = running } } @@ -216,15 +221,14 @@ class DesktopTasksController( return visualIndicator } - // TODO(b/347289970): Consider replacing with API - private fun isSystemUIApplication(taskInfo: RunningTaskInfo): Boolean { - return taskInfo.baseActivity?.packageName == sysUIPackageName - } - fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) { toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) - dragToDesktopTransitionHandler.setOnTaskResizeAnimatorListener(listener) + dragToDesktopTransitionHandler.onTaskResizeAnimationListener = listener + } + + fun setOnTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) { + returnToDragStartAnimator.setTaskRepositionAnimationListener(listener) } /** Setter needed to avoid cyclic dependency. */ @@ -233,15 +237,23 @@ class DesktopTasksController( dragToDesktopTransitionHandler.setSplitScreenController(controller) } + /** Returns the transition type for the given remote transition. */ + private fun transitionType(remoteTransition: RemoteTransition?): Int { + if (remoteTransition == null) { + logV("RemoteTransition is null") + return TRANSIT_NONE + } + return TRANSIT_TO_FRONT + } + /** Show all tasks, that are part of the desktop, on top of launcher */ fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition? = null) { - KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: showDesktopApps") + logV("showDesktopApps") val wct = WindowContainerTransaction() bringDesktopAppsToFront(displayId, wct) if (Transitions.ENABLE_SHELL_TRANSITIONS) { - // TODO(b/309014605): ensure remote transition is supplied once state is introduced - val transitionType = if (remoteTransition == null) TRANSIT_NONE else TRANSIT_TO_FRONT + val transitionType = transitionType(remoteTransition) val handler = remoteTransition?.let { OneShotRemoteHandler(transitions.mainExecutor, remoteTransition) @@ -254,121 +266,113 @@ class DesktopTasksController( } } - /** Get number of tasks that are marked as visible */ - fun getVisibleTaskCount(displayId: Int): Int { - return desktopModeTaskRepository.getVisibleTaskCount(displayId) - } + /** Gets number of visible tasks in [displayId]. */ + fun visibleTaskCount(displayId: Int): Int = + taskRepository.getVisibleTaskCount(displayId) - /** Enter desktop by using the focused task in given `displayId` */ + /** Returns true if any tasks are visible in Desktop Mode. */ + fun isDesktopModeShowing(displayId: Int): Boolean = visibleTaskCount(displayId) > 0 + + /** Moves focused task to desktop mode for given [displayId]. */ fun moveFocusedTaskToDesktop(displayId: Int, transitionSource: DesktopModeTransitionSource) { - val allFocusedTasks = - shellTaskOrganizer.getRunningTasks(displayId).filter { taskInfo -> - taskInfo.isFocused && - (taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN || - taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) && - taskInfo.activityType != ACTIVITY_TYPE_HOME - } - if (allFocusedTasks.isNotEmpty()) { - when (allFocusedTasks.size) { - 2 -> { - // Split-screen case where there are two focused tasks, then we find the child - // task to move to desktop. - val splitFocusedTask = - if (allFocusedTasks[0].taskId == allFocusedTasks[1].parentTaskId) { - allFocusedTasks[1] - } else { - allFocusedTasks[0] - } - moveToDesktop(splitFocusedTask, transitionSource = transitionSource) - } - 1 -> { - // Fullscreen case where we move the current focused task. - moveToDesktop(allFocusedTasks[0].taskId, transitionSource = transitionSource) - } - else -> { - KtProtoLog.w( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: Cannot enter desktop, expected less " + - "than 3 focused tasks but found %d", - allFocusedTasks.size - ) - } - } + val allFocusedTasks = getAllFocusedTasks(displayId) + when (allFocusedTasks.size) { + 0 -> return + // Full screen case + 1 -> moveRunningTaskToDesktop( + allFocusedTasks.single(), transitionSource = transitionSource) + // Split-screen case where there are two focused tasks, then we find the child + // task to move to desktop. + 2 -> moveRunningTaskToDesktop( + getSplitFocusedTask(allFocusedTasks[0], allFocusedTasks[1]), + transitionSource = transitionSource) + else -> logW( + "DesktopTasksController: Cannot enter desktop, expected less " + + "than 3 focused tasks but found %d", allFocusedTasks.size) } } - /** Move a task with given `taskId` to desktop */ - fun moveToDesktop( + /** + * Returns all focused tasks in full screen or split screen mode in [displayId] when + * it is not the home activity. + */ + private fun getAllFocusedTasks(displayId: Int): List<RunningTaskInfo> = + shellTaskOrganizer.getRunningTasks(displayId).filter { + it.isFocused && + (it.windowingMode == WINDOWING_MODE_FULLSCREEN || + it.windowingMode == WINDOWING_MODE_MULTI_WINDOW) && + it.activityType != ACTIVITY_TYPE_HOME + } + + /** Returns child task from two focused tasks in split screen mode. */ + private fun getSplitFocusedTask(task1: RunningTaskInfo, task2: RunningTaskInfo) = + if (task1.taskId == task2.parentTaskId) task2 else task1 + + private fun forceEnterDesktop(displayId: Int): Boolean { + if (!DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)) { + return false + } + + val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId) + requireNotNull(tdaInfo) { + "This method can only be called with the ID of a display having non-null DisplayArea." + } + val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode + val isFreeformDisplay = tdaWindowingMode == WINDOWING_MODE_FREEFORM + return isFreeformDisplay + } + + /** Moves task to desktop mode if task is running, else launches it in desktop mode. */ + fun moveTaskToDesktop( taskId: Int, wct: WindowContainerTransaction = WindowContainerTransaction(), transitionSource: DesktopModeTransitionSource, ): Boolean { - shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { - moveToDesktop(it, wct, transitionSource) + val runningTask = shellTaskOrganizer.getRunningTaskInfo(taskId) + if (runningTask == null) { + return moveBackgroundTaskToDesktop(taskId, wct, transitionSource) } - ?: moveToDesktopFromNonRunningTask(taskId, wct, transitionSource) + moveRunningTaskToDesktop(runningTask, wct, transitionSource) return true } - private fun moveToDesktopFromNonRunningTask( - taskId: Int, - wct: WindowContainerTransaction, - transitionSource: DesktopModeTransitionSource, + private fun moveBackgroundTaskToDesktop( + taskId: Int, + wct: WindowContainerTransaction, + transitionSource: DesktopModeTransitionSource, ): Boolean { - recentTasksController?.findTaskInBackground(taskId)?.let { - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: moveToDesktopFromNonRunningTask taskId=%d", - taskId - ) - // TODO(342378842): Instead of using default display, support multiple displays - val taskToMinimize = - bringDesktopAppsToFrontBeforeShowingNewTask(DEFAULT_DISPLAY, wct, taskId) - addMoveToDesktopChangesNonRunningTask(wct, taskId) - // TODO(343149901): Add DPI changes for task launch - val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource) - addPendingMinimizeTransition(transition, taskToMinimize) - return true + if (recentTasksController?.findTaskInBackground(taskId) == null) { + logW("moveBackgroundTaskToDesktop taskId=%d not found", taskId) + return false } - ?: return false - } - - private fun addMoveToDesktopChangesNonRunningTask( - wct: WindowContainerTransaction, - taskId: Int - ) { - val options = ActivityOptions.makeBasic() - options.launchWindowingMode = WINDOWING_MODE_FREEFORM - wct.startTask(taskId, options.toBundle()) + logV("moveBackgroundTaskToDesktop with taskId=%d", taskId) + // TODO(342378842): Instead of using default display, support multiple displays + val taskToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask( + DEFAULT_DISPLAY, wct, taskId) + wct.startTask( + taskId, + ActivityOptions.makeBasic().apply { + launchWindowingMode = WINDOWING_MODE_FREEFORM + }.toBundle(), + ) + // TODO(343149901): Add DPI changes for task launch + val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource) + addPendingMinimizeTransition(transition, taskToMinimize) + return true } - /** Move a task to desktop */ - fun moveToDesktop( + /** Moves a running task to desktop. */ + fun moveRunningTaskToDesktop( task: RunningTaskInfo, wct: WindowContainerTransaction = WindowContainerTransaction(), transitionSource: DesktopModeTransitionSource, ) { - if (Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task)) { - KtProtoLog.w( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: Cannot enter desktop, " + - "translucent top activity found. This is likely a modal dialog." - ) + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() + && isTopActivityExemptFromDesktopWindowing(context, task)) { + logW("Cannot enter desktop for taskId %d, ineligible top activity found", task.taskId) return } - if (isSystemUIApplication(task)) { - KtProtoLog.w( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: Cannot enter desktop, " + - "systemUI top activity found." - ) - return - } - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: moveToDesktop taskId=%d", - task.taskId - ) + logV("moveRunningTaskToDesktop taskId=%d", task.taskId) exitSplitIfApplicable(wct, task) // Bring other apps to front first val taskToMinimize = @@ -390,12 +394,11 @@ class DesktopTasksController( fun startDragToDesktop( taskInfo: RunningTaskInfo, dragToDesktopValueAnimator: MoveToDesktopAnimator, + taskSurface: SurfaceControl, ) { - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: startDragToDesktop taskId=%d", - taskInfo.taskId - ) + logV("startDragToDesktop taskId=%d", taskInfo.taskId) + interactionJankMonitor.begin(taskSurface, context, handler, + CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD) dragToDesktopTransitionHandler.startDragToDesktopTransition( taskInfo.taskId, dragToDesktopValueAnimator @@ -406,19 +409,18 @@ class DesktopTasksController( * The second part of the animated drag to desktop transition, called after * [startDragToDesktop]. */ - private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo, freeformBounds: Rect) { - KtProtoLog.v( + private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo) { + ProtoLog.v( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: finalizeDragToDesktop taskId=%d", taskInfo.taskId ) val wct = WindowContainerTransaction() exitSplitIfApplicable(wct, taskInfo) - moveHomeTaskToFront(wct) + moveHomeTask(wct, toTop = true) val taskToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId) addMoveToDesktopChanges(wct, taskInfo) - wct.setBounds(taskInfo.token, freeformBounds) val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct) transition?.let { addPendingMinimizeTransition(it, taskToMinimize) } } @@ -442,12 +444,28 @@ class DesktopTasksController( * active task. * * @param wct transaction to modify if the last active task is closed + * @param displayId display id of the window that's being closed * @param taskId task id of the window that's being closed */ - fun onDesktopWindowClose(wct: WindowContainerTransaction, taskId: Int) { - if (desktopModeTaskRepository.isOnlyActiveTask(taskId)) { + fun onDesktopWindowClose(wct: WindowContainerTransaction, displayId: Int, taskId: Int) { + if (taskRepository.isOnlyVisibleNonClosingTask(taskId)) { removeWallpaperActivity(wct) } + taskRepository.addClosingTask(displayId, taskId) + } + + /** + * Perform clean up of the desktop wallpaper activity if the minimized window task is the last + * active task. + * + * @param wct transaction to modify if the last active task is minimized + * @param taskId task id of the window that's being minimized + */ + fun onDesktopWindowMinimize(wct: WindowContainerTransaction, taskId: Int) { + if (taskRepository.isOnlyVisibleNonClosingTask(taskId)) { + removeWallpaperActivity(wct) + } + // Do not call taskRepository.minimizeTask because it will be called by DekstopTasksLimiter. } /** Move a task with given `taskId` to fullscreen */ @@ -466,11 +484,7 @@ class DesktopTasksController( /** Move a desktop app to split screen. */ fun moveToSplit(task: RunningTaskInfo) { - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: moveToSplit taskId=%d", - task.taskId - ) + logV( "moveToSplit taskId=%s", task.taskId) val wct = WindowContainerTransaction() wct.setBounds(task.token, Rect()) // Rather than set windowing mode to multi-window at task level, set it to @@ -499,11 +513,7 @@ class DesktopTasksController( * [startDragToDesktop]. */ fun cancelDragToDesktop(task: RunningTaskInfo) { - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: cancelDragToDesktop taskId=%d", - task.taskId - ) + logV("cancelDragToDesktop taskId=%d", task.taskId) dragToDesktopTransitionHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL ) @@ -514,11 +524,7 @@ class DesktopTasksController( position: Point, transitionSource: DesktopModeTransitionSource ) { - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: moveToFullscreen with animation taskId=%d", - task.taskId - ) + logV("moveToFullscreenWithAnimation taskId=%d", task.taskId) val wct = WindowContainerTransaction() addMoveToFullscreenChanges(wct, task) @@ -542,12 +548,7 @@ class DesktopTasksController( /** Move a task to the front */ fun moveTaskToFront(taskInfo: RunningTaskInfo) { - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: moveTaskToFront taskId=%d", - taskInfo.taskId - ) - + logV("moveTaskToFront taskId=%s", taskInfo.taskId) val wct = WindowContainerTransaction() wct.reorder(taskInfo.token, true) val taskToMinimize = addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo) @@ -573,15 +574,10 @@ class DesktopTasksController( fun moveToNextDisplay(taskId: Int) { val task = shellTaskOrganizer.getRunningTaskInfo(taskId) if (task == null) { - KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d not found", taskId) + logW("moveToNextDisplay: taskId=%d not found", taskId) return } - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "moveToNextDisplay: taskId=%d taskDisplayId=%d", - taskId, - task.displayId - ) + logV("moveToNextDisplay: taskId=%d displayId=%d", taskId, task.displayId) val displayIds = rootTaskDisplayAreaOrganizer.displayIds.sorted() // Get the first display id that is higher than current task display id @@ -591,7 +587,7 @@ class DesktopTasksController( newDisplayId = displayIds.firstOrNull { displayId -> displayId < task.displayId } } if (newDisplayId == null) { - KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: next display not found") + logW("moveToNextDisplay: next display not found") return } moveToDisplay(task, newDisplayId) @@ -603,21 +599,15 @@ class DesktopTasksController( * No-op if task is already on that display per [RunningTaskInfo.displayId]. */ private fun moveToDisplay(task: RunningTaskInfo, displayId: Int) { - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "moveToDisplay: taskId=%d displayId=%d", - task.taskId, - displayId - ) - + logV("moveToDisplay: taskId=%d displayId=%d", task.taskId, displayId) if (task.displayId == displayId) { - KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "moveToDisplay: task already on display") + logD("moveToDisplay: task already on display %d", displayId) return } val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId) if (displayAreaInfo == null) { - KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToDisplay: display not found") + logW("moveToDisplay: display not found") return } @@ -638,19 +628,24 @@ class DesktopTasksController( fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo) { val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return - val stableBounds = Rect() - displayLayout.getStableBounds(stableBounds) + val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } + val currentTaskBounds = taskInfo.configuration.windowConfiguration.bounds val destinationBounds = Rect() - if (taskInfo.configuration.windowConfiguration.bounds == stableBounds) { - // The desktop task is currently occupying the whole stable bounds. If the bounds - // before the task was toggled to stable bounds were saved, toggle the task to those - // bounds. Otherwise, toggle to the default bounds. + + val isMaximized = isTaskMaximized(taskInfo, stableBounds) + // If the task is currently maximized, we will toggle it not to be and vice versa. This is + // helpful to eliminate the current task from logic to calculate taskbar corner rounding. + val willMaximize = !isMaximized + if (isMaximized) { + // The desktop task is at the maximized width and/or height of the stable bounds. + // If the task's pre-maximize stable bounds were saved, toggle the task to those bounds. + // Otherwise, toggle to the default bounds. val taskBoundsBeforeMaximize = - desktopModeTaskRepository.removeBoundsBeforeMaximize(taskInfo.taskId) + taskRepository.removeBoundsBeforeMaximize(taskInfo.taskId) if (taskBoundsBeforeMaximize != null) { destinationBounds.set(taskBoundsBeforeMaximize) } else { - if (Flags.enableWindowingDynamicInitialBounds()) { + if (ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isTrue()) { destinationBounds.set(calculateInitialBounds(displayLayout, taskInfo)) } else { destinationBounds.set(getDefaultDesktopTaskBounds(displayLayout)) @@ -659,11 +654,34 @@ class DesktopTasksController( } else { // Save current bounds so that task can be restored back to original bounds if necessary // and toggle to the stable bounds. - val taskBounds = taskInfo.configuration.windowConfiguration.bounds - desktopModeTaskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, taskBounds) - destinationBounds.set(stableBounds) + taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds) + + if (taskInfo.isResizeable) { + // if resizable then expand to entire stable bounds (full display minus insets) + destinationBounds.set(stableBounds) + } else { + // if non-resizable then calculate max bounds according to aspect ratio + val activityAspectRatio = calculateAspectRatio(taskInfo) + val newSize = maximizeSizeGivenAspectRatio(taskInfo, + Size(stableBounds.width(), stableBounds.height()), activityAspectRatio) + val newBounds = centerInArea( + newSize, stableBounds, stableBounds.left, stableBounds.top) + destinationBounds.set(newBounds) + } } + + + val shouldRestoreToSnap = + isMaximized && isTaskSnappedToHalfScreen(taskInfo, destinationBounds) + + logD("willMaximize = %s", willMaximize) + logD("shouldRestoreToSnap = %s", shouldRestoreToSnap) + + val doesAnyTaskRequireTaskbarRounding = willMaximize || shouldRestoreToSnap || + doesAnyTaskRequireTaskbarRounding(taskInfo.displayId, taskInfo.taskId) + + taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding) val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds) if (Transitions.ENABLE_SHELL_TRANSITIONS) { toggleResizeDesktopTaskTransitionHandler.startTransition(wct) @@ -672,24 +690,136 @@ class DesktopTasksController( } } + private fun isTaskMaximized( + taskInfo: RunningTaskInfo, + stableBounds: Rect + ): Boolean { + val currentTaskBounds = taskInfo.configuration.windowConfiguration.bounds + + return if (taskInfo.isResizeable) { + isTaskBoundsEqual(currentTaskBounds, stableBounds) + } else { + isTaskWidthOrHeightEqual(currentTaskBounds, stableBounds) + } + } + + private fun isMaximizedToStableBoundsEdges( + taskInfo: RunningTaskInfo, + stableBounds: Rect + ): Boolean { + val currentTaskBounds = taskInfo.configuration.windowConfiguration.bounds + return isTaskBoundsEqual(currentTaskBounds, stableBounds) + } + + /** Returns if current task bound is snapped to half screen */ + private fun isTaskSnappedToHalfScreen( + taskInfo: RunningTaskInfo, + taskBounds: Rect = taskInfo.configuration.windowConfiguration.bounds + ): Boolean = + getSnapBounds(taskInfo, SnapPosition.LEFT) == taskBounds || + getSnapBounds(taskInfo, SnapPosition.RIGHT) == taskBounds + + @VisibleForTesting + fun doesAnyTaskRequireTaskbarRounding( + displayId: Int, + excludeTaskId: Int? = null, + ): Boolean { + val doesAnyTaskRequireTaskbarRounding = + taskRepository.getActiveNonMinimizedOrderedTasks(displayId) + // exclude current task since maximize/restore transition has not taken place yet. + .filterNot { taskId -> taskId == excludeTaskId } + .any { taskId -> + val taskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId) ?: return false + val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) + val stableBounds = Rect().apply { displayLayout?.getStableBounds(this) } + logD("taskInfo = %s", taskInfo) + logD( + "isTaskSnappedToHalfScreen(taskInfo) = %s", + isTaskSnappedToHalfScreen(taskInfo) + ) + logD( + "isMaximizedToStableBoundsEdges(taskInfo, stableBounds) = %s", + isMaximizedToStableBoundsEdges(taskInfo, stableBounds) + ) + isTaskSnappedToHalfScreen(taskInfo) + || isMaximizedToStableBoundsEdges(taskInfo, stableBounds) + } + + logD("doesAnyTaskRequireTaskbarRounding = %s", doesAnyTaskRequireTaskbarRounding) + return doesAnyTaskRequireTaskbarRounding + } + /** * Quick-resize to the right or left half of the stable bounds. * + * @param taskInfo current task that is being snap-resized via dragging or maximize menu button + * @param taskSurface the leash of the task being dragged + * @param currentDragBounds current position of the task leash being dragged (or current task + * bounds if being snapped resize via maximize menu button) * @param position the portion of the screen (RIGHT or LEFT) we want to snap the task to. */ - fun snapToHalfScreen(taskInfo: RunningTaskInfo, position: SnapPosition) { + fun snapToHalfScreen( + taskInfo: RunningTaskInfo, + taskSurface: SurfaceControl, + currentDragBounds: Rect, + position: SnapPosition + ) { val destinationBounds = getSnapBounds(taskInfo, position) + if (destinationBounds == taskInfo.configuration.windowConfiguration.bounds) { + // Handle the case where we attempt to snap resize when already snap resized: the task + // position won't need to change but we want to animate the surface going back to the + // snapped position from the "dragged-to-the-edge" position. + if (destinationBounds != currentDragBounds) { + returnToDragStartAnimator.start( + taskInfo.taskId, + taskSurface, + startBounds = currentDragBounds, + endBounds = destinationBounds, + isResizable = taskInfo.isResizeable + ) + } + return + } - if (destinationBounds == taskInfo.configuration.windowConfiguration.bounds) return - + taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(true) val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds) if (Transitions.ENABLE_SHELL_TRANSITIONS) { - toggleResizeDesktopTaskTransitionHandler.startTransition(wct) + toggleResizeDesktopTaskTransitionHandler.startTransition(wct, currentDragBounds) } else { shellTaskOrganizer.applyTransaction(wct) } } + @VisibleForTesting + fun handleSnapResizingTask( + taskInfo: RunningTaskInfo, + position: SnapPosition, + taskSurface: SurfaceControl, + currentDragBounds: Rect, + dragStartBounds: Rect + ) { + releaseVisualIndicator() + if (!taskInfo.isResizeable && DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE.isTrue()) { + interactionJankMonitor.begin( + taskSurface, context, handler, CUJ_DESKTOP_MODE_SNAP_RESIZE, "drag_non_resizable" + ) + + // reposition non-resizable app back to its original position before being dragged + returnToDragStartAnimator.start( + taskInfo.taskId, + taskSurface, + startBounds = currentDragBounds, + endBounds = dragStartBounds, + isResizable = taskInfo.isResizeable, + ) + } else { + interactionJankMonitor.begin( + taskSurface, context, handler, CUJ_DESKTOP_MODE_SNAP_RESIZE, "drag_resizable" + ) + snapToHalfScreen(taskInfo, taskSurface, currentDragBounds, position) + } + } + private fun getDefaultDesktopTaskBounds(displayLayout: DisplayLayout): Rect { // TODO(b/319819547): Account for app constraints so apps do not become letterboxed val desiredWidth = (displayLayout.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE).toInt() @@ -753,23 +883,21 @@ class DesktopTasksController( wct: WindowContainerTransaction, newTaskIdInFront: Int? = null ): RunningTaskInfo? { - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: bringDesktopAppsToFront, newTaskIdInFront=%s", - newTaskIdInFront ?: "null" - ) + logV("bringDesktopAppsToFront, newTaskId=%d", newTaskIdInFront) + // Move home to front, ensures that we go back home when all desktop windows are closed + moveHomeTask(wct, toTop = true) - if (Flags.enableDesktopWindowingWallpaperActivity()) { + // Currently, we only handle the desktop on the default display really. + if (displayId == DEFAULT_DISPLAY + && ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) { // Add translucent wallpaper activity to show the wallpaper underneath addWallpaperActivity(wct) - } else { - // Move home to front - moveHomeTaskToFront(wct) } val nonMinimizedTasksOrderedFrontToBack = - desktopModeTaskRepository.getActiveNonMinimizedTasksOrderedFrontToBack(displayId) + taskRepository.getActiveNonMinimizedOrderedTasks(displayId) // If we're adding a new Task we might need to minimize an old one + // TODO(b/365725441): Handle non running task minimization val taskToMinimize: RunningTaskInfo? = if (newTaskIdInFront != null && desktopTasksLimiter.isPresent) { desktopTasksLimiter @@ -781,30 +909,48 @@ class DesktopTasksController( } else { null } + nonMinimizedTasksOrderedFrontToBack // If there is a Task to minimize, let it stay behind the Home Task .filter { taskId -> taskId != taskToMinimize?.taskId } - .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) } .reversed() // Start from the back so the front task is brought forward last - .forEach { task -> wct.reorder(task.token, true /* onTop */) } + .forEach { taskId -> + val runningTaskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId) + if (runningTaskInfo != null) { + // Task is already running, reorder it to the front + wct.reorder(runningTaskInfo.token, /* onTop= */ true) + } else if (Flags.enableDesktopWindowingPersistence()) { + // Task is not running, start it + wct.startTask( + taskId, + ActivityOptions.makeBasic().apply { + launchWindowingMode = WINDOWING_MODE_FREEFORM + }.toBundle(), + ) + } + } + + taskbarDesktopTaskListener?. + onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding(displayId)) + return taskToMinimize } - private fun moveHomeTaskToFront(wct: WindowContainerTransaction) { + private fun moveHomeTask(wct: WindowContainerTransaction, toTop: Boolean) { shellTaskOrganizer .getRunningTasks(context.displayId) .firstOrNull { task -> task.activityType == ACTIVITY_TYPE_HOME } - ?.let { homeTask -> wct.reorder(homeTask.getToken(), true /* onTop */) } + ?.let { homeTask -> wct.reorder(homeTask.getToken(), /* onTop= */ toTop) } } private fun addWallpaperActivity(wct: WindowContainerTransaction) { - KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: addWallpaper") + logV("addWallpaperActivity") val intent = Intent(context, DesktopWallpaperActivity::class.java) val options = ActivityOptions.makeBasic().apply { - isPendingIntentBackgroundActivityLaunchAllowedByPermission = true + launchWindowingMode = WINDOWING_MODE_FULLSCREEN pendingIntentBackgroundActivityStartMode = - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS } val pendingIntent = PendingIntent.getActivity( @@ -817,8 +963,8 @@ class DesktopTasksController( } private fun removeWallpaperActivity(wct: WindowContainerTransaction) { - desktopModeTaskRepository.wallpaperActivityToken?.let { token -> - KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: removeWallpaper") + taskRepository.wallpaperActivityToken?.let { token -> + logV("removeWallpaperActivity") wct.removeTask(token) } } @@ -856,22 +1002,30 @@ class DesktopTasksController( transition: IBinder, request: TransitionRequestInfo ): WindowContainerTransaction? { - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: handleRequest request=%s", - request - ) + logV("handleRequest request=%s", request) // Check if we should skip handling this transition var reason = "" val triggerTask = request.triggerTask + var shouldHandleMidRecentsFreeformLaunch = + recentsAnimationRunning && isFreeformRelaunch(triggerTask, request) + val isDragAndDropFullscreenTransition = taskContainsDragAndDropCookie(triggerTask) val shouldHandleRequest = when { + // Handle freeform relaunch during recents animation + shouldHandleMidRecentsFreeformLaunch -> true recentsAnimationRunning -> { reason = "recents animation is running" false } - // Handle back navigation for the last window if wallpaper available - shouldRemoveWallpaper(request) -> true + // Don't handle request if this was a tear to fullscreen transition. + // handleFullscreenTaskLaunch moves fullscreen intents to freeform; + // this is an exception to the rule + isDragAndDropFullscreenTransition -> { + dragAndDropFullscreenCookie = null + false + } + // Handle task closing for the last window if wallpaper is available + shouldHandleTaskClosing(request) -> true // Only handle open or to front transitions request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> { reason = "transition type not handled (${request.type})" @@ -888,8 +1042,7 @@ class DesktopTasksController( false } // Only handle fullscreen or freeform tasks - triggerTask.windowingMode != WINDOWING_MODE_FULLSCREEN && - triggerTask.windowingMode != WINDOWING_MODE_FREEFORM -> { + !triggerTask.isFullscreen && !triggerTask.isFreeform -> { reason = "windowingMode not handled (${triggerTask.windowingMode})" false } @@ -898,22 +1051,19 @@ class DesktopTasksController( } if (!shouldHandleRequest) { - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: skipping handleRequest reason=%s", - reason - ) + logV("skipping handleRequest reason=%s", reason) return null } val result = triggerTask?.let { task -> when { - request.type == TRANSIT_TO_BACK -> handleBackNavigation(task) - // Check if the task has a top transparent activity - shouldLaunchAsModal(task) -> handleIncompatibleTaskLaunch(task) - // Check if the task has a top systemUI activity - isSystemUIApplication(task) -> handleIncompatibleTaskLaunch(task) + // Check if freeform task launch during recents should be handled + shouldHandleMidRecentsFreeformLaunch -> handleMidRecentsFreeformTaskLaunch(task) + // Check if the closing task needs to be handled + TransitionUtil.isClosingType(request.type) -> handleTaskClosing(task) + // Check if the top task shouldn't be allowed to enter desktop mode + isIncompatibleTask(task) -> handleIncompatibleTaskLaunch(task) // Check if fullscreen task should be updated task.isFullscreen -> handleFullscreenTaskLaunch(task, transition) // Check if freeform task should be updated @@ -923,14 +1073,13 @@ class DesktopTasksController( } } } - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: handleRequest result=%s", - result ?: "null" - ) + logV("handleRequest result=%s", result) return result } + private fun taskContainsDragAndDropCookie(taskInfo: RunningTaskInfo?) = + taskInfo?.launchCookies?.any { it == dragAndDropFullscreenCookie } ?: false + /** * Applies the proper surface states (rounded corners) to tasks when desktop mode is active. * This is intended to be used when desktop mode is part of another animation but isn't, itself, @@ -947,37 +1096,149 @@ class DesktopTasksController( .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) } } - // TODO(b/347289970): Consider replacing with API - private fun shouldLaunchAsModal(task: TaskInfo) = - Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task) + /** Returns whether an existing desktop task is being relaunched in freeform or not. */ + private fun isFreeformRelaunch(triggerTask: RunningTaskInfo?, request: TransitionRequestInfo) = + (triggerTask != null && triggerTask.windowingMode == WINDOWING_MODE_FREEFORM + && TransitionUtil.isOpeningType(request.type) + && taskRepository.isActiveTask(triggerTask.taskId)) + + private fun isIncompatibleTask(task: TaskInfo) = + DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() + && isTopActivityExemptFromDesktopWindowing(context, task) + + private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean { + return ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() && + TransitionUtil.isClosingType(request.type) && + request.triggerTask != null + } - private fun shouldRemoveWallpaper(request: TransitionRequestInfo): Boolean { - return Flags.enableDesktopWindowingWallpaperActivity() && - request.type == TRANSIT_TO_BACK && - request.triggerTask?.let { task -> - desktopModeTaskRepository.isOnlyActiveTask(task.taskId) + /** Open an existing instance of an app. */ + fun openInstance( + callingTask: RunningTaskInfo, + requestedTaskId: Int + ) { + val wct = WindowContainerTransaction() + val options = createNewWindowOptions(callingTask) + if (options.launchWindowingMode == WINDOWING_MODE_FREEFORM) { + wct.startTask(requestedTaskId, options.toBundle()) + transitions.startTransition(TRANSIT_OPEN, wct, null) + } else { + val splitPosition = splitScreenController.determineNewInstancePosition(callingTask) + splitScreenController.startTask(requestedTaskId, splitPosition, + options.toBundle(), null /* hideTaskToken */) + } + } + + /** Create an Intent to open a new window of a task. */ + fun openNewWindow( + callingTaskInfo: RunningTaskInfo + ) { + // TODO(b/337915660): Add a transition handler for these; animations + // need updates in some cases. + val baseActivity = callingTaskInfo.baseActivity ?: return + val fillIn: Intent = context.packageManager + .getLaunchIntentForPackage( + baseActivity.packageName + ) ?: return + fillIn + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) + val launchIntent = PendingIntent.getActivity( + context, + /* requestCode= */ 0, + fillIn, + PendingIntent.FLAG_IMMUTABLE + ) + val options = createNewWindowOptions(callingTaskInfo) + when (options.launchWindowingMode) { + WINDOWING_MODE_MULTI_WINDOW -> { + val splitPosition = splitScreenController + .determineNewInstancePosition(callingTaskInfo) + splitScreenController.startIntent( + launchIntent, context.userId, fillIn, splitPosition, + options.toBundle(), null /* hideTaskToken */ + ) } - ?: false + WINDOWING_MODE_FREEFORM -> { + // TODO(b/336289597): This currently does not respect the desktop window limit. + val wct = WindowContainerTransaction() + wct.sendPendingIntent(launchIntent, fillIn, options.toBundle()) + transitions.startTransition(TRANSIT_OPEN, wct, null) + } + } + } + + private fun createNewWindowOptions(callingTask: RunningTaskInfo): ActivityOptions { + val newTaskWindowingMode = when { + callingTask.isFreeform -> { + WINDOWING_MODE_FREEFORM + } + callingTask.isFullscreen || callingTask.isMultiWindow -> { + WINDOWING_MODE_MULTI_WINDOW + } + else -> { + error("Invalid windowing mode: ${callingTask.windowingMode}") + } + } + return ActivityOptions.makeBasic().apply { + launchWindowingMode = newTaskWindowingMode + pendingIntentBackgroundActivityStartMode = + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS + } + } + + /** + * Handles the case where a freeform task is launched from recents. + * + * This is a special case where we want to launch the task in fullscreen instead of freeform. + */ + private fun handleMidRecentsFreeformTaskLaunch( + task: RunningTaskInfo + ): WindowContainerTransaction? { + logV("DesktopTasksController: handleMidRecentsFreeformTaskLaunch") + val wct = WindowContainerTransaction() + addMoveToFullscreenChanges(wct, task) + wct.reorder(task.token, true) + return wct } private fun handleFreeformTaskLaunch( task: RunningTaskInfo, transition: IBinder ): WindowContainerTransaction? { - KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFreeformTaskLaunch") - if (!desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) { - KtProtoLog.d( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: switch freeform task to fullscreen oon transition" + - " taskId=%d", - task.taskId - ) - return WindowContainerTransaction().also { wct -> - bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) - wct.reorder(task.token, true) - } + logV("handleFreeformTaskLaunch") + if (keyguardManager.isKeyguardLocked) { + // Do NOT handle freeform task launch when locked. + // It will be launched in fullscreen windowing mode (Details: b/160925539) + logV("skip keyguard is locked") + return null } val wct = WindowContainerTransaction() + if (!isDesktopModeShowing(task.displayId)) { + logD("Bring desktop tasks to front on transition=taskId=%d", task.taskId) + if (taskRepository.isActiveTask(task.taskId) && !forceEnterDesktop(task.displayId)) { + // We are outside of desktop mode and already existing desktop task is being + // launched. We should make this task go to fullscreen instead of freeform. Note + // that this means any re-launch of a freeform window outside of desktop will be in + // fullscreen as long as default-desktop flag is disabled. + addMoveToFullscreenChanges(wct, task) + return wct + } + bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) + wct.reorder(task.token, true) + return wct + } + // TODO(b/365723620): Handle non running tasks that were launched after reboot. + // If task is already visible, it must have been handled already and added to desktop mode. + // Cascade task only if it's not visible yet. + if (DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue() + && !taskRepository.isVisibleTask(task.taskId)) { + val displayLayout = displayController.getDisplayLayout(task.displayId) + if (displayLayout != null) { + val initialBounds = Rect(task.configuration.windowConfiguration.bounds) + cascadeWindow(task, initialBounds, displayLayout) + wct.setBounds(task.token, initialBounds) + } + } if (useDesktopOverrideDensity()) { wct.setDensityDpi(task.token, DESKTOP_DENSITY_OVERRIDE) } @@ -995,16 +1256,18 @@ class DesktopTasksController( task: RunningTaskInfo, transition: IBinder ): WindowContainerTransaction? { - KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFullscreenTaskLaunch") - if (desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) { - KtProtoLog.d( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: switch fullscreen task to freeform on transition" + - " taskId=%d", - task.taskId - ) + logV("handleFullscreenTaskLaunch") + if (isDesktopModeShowing(task.displayId) || forceEnterDesktop(task.displayId)) { + logD("Switch fullscreen task to freeform on transition: taskId=%d", task.taskId) return WindowContainerTransaction().also { wct -> addMoveToDesktopChanges(wct, task) + // In some launches home task is moved behind new task being launched. Make sure + // that's not the case for launches in desktop. + if (task.baseIntent.flags.and(Intent.FLAG_ACTIVITY_TASK_ON_HOME) != 0) { + bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) + wct.reorder(task.token, true) + } + // Desktop Mode is already showing and we're launching a new Task - we might need to // minimize another Task. val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task) @@ -1024,20 +1287,37 @@ class DesktopTasksController( return WindowContainerTransaction().also { wct -> addMoveToFullscreenChanges(wct, task) } } - /** Handle back navigation by removing wallpaper activity if it's the last active task */ - private fun handleBackNavigation(task: RunningTaskInfo): WindowContainerTransaction? { - if ( - desktopModeTaskRepository.isOnlyActiveTask(task.taskId) && - desktopModeTaskRepository.wallpaperActivityToken != null + /** Handle task closing by removing wallpaper activity if it's the last active task */ + private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? { + logV("handleTaskClosing") + if (!isDesktopModeShowing(task.displayId)) + return null + + val wct = WindowContainerTransaction() + if (taskRepository.isOnlyVisibleNonClosingTask(task.taskId) + && taskRepository.wallpaperActivityToken != null ) { // Remove wallpaper activity when the last active task is removed - return WindowContainerTransaction().also { wct -> removeWallpaperActivity(wct) } - } else { - return null + removeWallpaperActivity(wct) } + taskRepository.addClosingTask(task.displayId, task.taskId) + // If a CLOSE or TO_BACK is triggered on a desktop task, remove the task. + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue() && + taskRepository.isVisibleTask(task.taskId) + ) { + wct.removeTask(task.token) + } + taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( + doesAnyTaskRequireTaskbarRounding( + task.displayId, + task.id + ) + ) + return if (wct.isEmpty) null else wct } - private fun addMoveToDesktopChanges( + @VisibleForTesting + fun addMoveToDesktopChanges( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo ) { @@ -1051,8 +1331,18 @@ class DesktopTasksController( } else { WINDOWING_MODE_FREEFORM } - if (Flags.enableWindowingDynamicInitialBounds()) { - wct.setBounds(taskInfo.token, calculateInitialBounds(displayLayout, taskInfo)) + val initialBounds = if (ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isTrue()) { + calculateInitialBounds(displayLayout, taskInfo) + } else { + getDefaultDesktopTaskBounds(displayLayout) + } + + if (DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue()) { + cascadeWindow(taskInfo, initialBounds, displayLayout) + } + + if (canChangeTaskPosition(taskInfo)) { + wct.setBounds(taskInfo.token, initialBounds) } wct.setWindowingMode(taskInfo.token, targetWindowingMode) wct.reorder(taskInfo.token, true /* onTop */) @@ -1079,6 +1369,23 @@ class DesktopTasksController( if (useDesktopOverrideDensity()) { wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) } + if (taskRepository.isOnlyVisibleNonClosingTask(taskInfo.taskId)) { + // Remove wallpaper activity when leaving desktop mode + removeWallpaperActivity(wct) + } + } + + private fun cascadeWindow(task: TaskInfo, bounds: Rect, displayLayout: DisplayLayout) { + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + val activeTasks = taskRepository.getActiveNonMinimizedOrderedTasks(task.displayId) + activeTasks.firstOrNull()?.let { activeTask -> + shellTaskOrganizer.getRunningTaskInfo(activeTask)?.let { + cascadeWindow(context.resources, stableBounds, + it.configuration.windowConfiguration.bounds, bounds) + } + } } /** @@ -1094,6 +1401,10 @@ class DesktopTasksController( // The task's density may have been overridden in freeform; revert it here as we don't // want it overridden in multi-window. wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) + if (taskRepository.isOnlyVisibleNonClosingTask(taskInfo.taskId)) { + // Remove wallpaper activity when leaving desktop mode + removeWallpaperActivity(wct) + } } /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */ @@ -1196,14 +1507,16 @@ class DesktopTasksController( taskBounds: Rect ) { if (taskInfo.windowingMode != WINDOWING_MODE_FREEFORM) return - updateVisualIndicator(taskInfo, taskSurface, inputX, taskBounds.top.toFloat()) + updateVisualIndicator(taskInfo, taskSurface, inputX, taskBounds.top.toFloat(), + DragStartState.FROM_FREEFORM) } fun updateVisualIndicator( taskInfo: RunningTaskInfo, - taskSurface: SurfaceControl, + taskSurface: SurfaceControl?, inputX: Float, - taskTop: Float + taskTop: Float, + dragStartState: DragStartState ): DesktopModeVisualIndicator.IndicatorType { // If the visual indicator does not exist, create it. val indicator = @@ -1214,10 +1527,11 @@ class DesktopTasksController( displayController, context, taskSurface, - rootTaskDisplayAreaOrganizer + rootTaskDisplayAreaOrganizer, + dragStartState ) if (visualIndicator == null) visualIndicator = indicator - return indicator.updateIndicatorType(PointF(inputX, taskTop), taskInfo.windowingMode) + return indicator.updateIndicatorType(PointF(inputX, taskTop)) } /** @@ -1225,16 +1539,22 @@ class DesktopTasksController( * that change. Otherwise, ensure bounds are up to date. * * @param taskInfo the task being dragged. + * @param taskSurface the leash of the task being dragged. * @param position position of surface when drag ends. * @param inputCoordinate the coordinates of the motion event - * @param taskBounds the updated bounds of the task being dragged. + * @param currentDragBounds the current bounds of where the visible task is (might be actual + * task bounds or just task leash) + * @param validDragArea the bounds of where the task can be dragged within the display. + * @param dragStartBounds the bounds of the task before starting dragging. */ fun onDragPositioningEnd( taskInfo: RunningTaskInfo, + taskSurface: SurfaceControl, position: Point, inputCoordinate: PointF, - taskBounds: Rect, - validDragArea: Rect + currentDragBounds: Rect, + validDragArea: Rect, + dragStartBounds: Rect, ) { if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) { return @@ -1243,41 +1563,43 @@ class DesktopTasksController( val indicator = visualIndicator ?: return val indicatorType = indicator.updateIndicatorType( - PointF(inputCoordinate.x, taskBounds.top.toFloat()), - taskInfo.windowingMode + PointF(inputCoordinate.x, currentDragBounds.top.toFloat()), ) when (indicatorType) { - DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> { + IndicatorType.TO_FULLSCREEN_INDICATOR -> { moveToFullscreenWithAnimation( taskInfo, position, DesktopModeTransitionSource.TASK_DRAG ) } - DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { - releaseVisualIndicator() - snapToHalfScreen(taskInfo, SnapPosition.LEFT) + IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { + handleSnapResizingTask( + taskInfo, SnapPosition.LEFT, taskSurface, currentDragBounds, dragStartBounds + ) } - DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { - releaseVisualIndicator() - snapToHalfScreen(taskInfo, SnapPosition.RIGHT) + IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { + handleSnapResizingTask( + taskInfo, SnapPosition.RIGHT, taskSurface, currentDragBounds, dragStartBounds + ) } - DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR -> { - // If task bounds are outside valid drag area, snap them inward and perform a - // transaction to set bounds. - if ( - DragPositioningCallbackUtility.snapTaskBoundsIfNecessary( - taskBounds, - validDragArea - ) - ) { - val wct = WindowContainerTransaction() - wct.setBounds(taskInfo.token, taskBounds) - transitions.startTransition(TRANSIT_CHANGE, wct, null) - } + IndicatorType.NO_INDICATOR -> { + // If task bounds are outside valid drag area, snap them inward + DragPositioningCallbackUtility.snapTaskBoundsIfNecessary( + currentDragBounds, + validDragArea + ) + + if (currentDragBounds == dragStartBounds) return + + // Update task bounds so that the task position will match the position of its leash + val wct = WindowContainerTransaction() + wct.setBounds(taskInfo.token, currentDragBounds) + transitions.startTransition(TRANSIT_CHANGE, wct, null) + releaseVisualIndicator() } - DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> { + IndicatorType.TO_DESKTOP_INDICATOR -> { throw IllegalArgumentException( "Should not be receiving TO_DESKTOP_INDICATOR for " + "a freeform task." ) @@ -1285,6 +1607,8 @@ class DesktopTasksController( } // A freeform drag-move ended, remove the indicator immediately. releaseVisualIndicator() + taskbarDesktopTaskListener + ?.onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding(taskInfo.displayId)) } /** @@ -1292,43 +1616,46 @@ class DesktopTasksController( * * @param taskInfo the task being dragged. * @param y height of drag, to be checked against status bar height. + * @return the [IndicatorType] used for the resulting transition */ fun onDragPositioningEndThroughStatusBar( inputCoordinates: PointF, taskInfo: RunningTaskInfo, - ) { - val indicator = getVisualIndicator() ?: return - val indicatorType = indicator.updateIndicatorType(inputCoordinates, taskInfo.windowingMode) + taskSurface: SurfaceControl, + ): IndicatorType { + // End the drag_hold CUJ interaction. + interactionJankMonitor.end(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD) + val indicator = getVisualIndicator() ?: return IndicatorType.NO_INDICATOR + val indicatorType = indicator.updateIndicatorType(inputCoordinates) when (indicatorType) { - DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> { - val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return - if (Flags.enableWindowingDynamicInitialBounds()) { - finalizeDragToDesktop(taskInfo, calculateInitialBounds(displayLayout, taskInfo)) - } else { - finalizeDragToDesktop(taskInfo, getDefaultDesktopTaskBounds(displayLayout)) - } + IndicatorType.TO_DESKTOP_INDICATOR -> { + // Start a new jank interaction for the drag release to desktop window animation. + interactionJankMonitor.begin(taskSurface, context, handler, + CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE, "to_desktop") + finalizeDragToDesktop(taskInfo) } - DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR, - DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> { + IndicatorType.NO_INDICATOR, + IndicatorType.TO_FULLSCREEN_INDICATOR -> { cancelDragToDesktop(taskInfo) } - DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { + IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { requestSplit(taskInfo, leftOrTop = true) } - DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { + IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { requestSplit(taskInfo, leftOrTop = false) } } + return indicatorType } /** Update the exclusion region for a specified task */ fun onExclusionRegionChanged(taskId: Int, exclusionRegion: Region) { - desktopModeTaskRepository.updateTaskExclusionRegions(taskId, exclusionRegion) + taskRepository.updateTaskExclusionRegions(taskId, exclusionRegion) } /** Remove a previously tracked exclusion region for a specified task. */ fun removeExclusionRegionForTask(taskId: Int) { - desktopModeTaskRepository.removeExclusionRegion(taskId) + taskRepository.removeExclusionRegion(taskId) } /** @@ -1338,7 +1665,7 @@ class DesktopTasksController( * @param callbackExecutor the executor to call the listener on. */ fun addVisibleTasksListener(listener: VisibleTasksListener, callbackExecutor: Executor) { - desktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor) + taskRepository.addVisibleTasksListener(listener, callbackExecutor) } /** @@ -1348,43 +1675,90 @@ class DesktopTasksController( * @param callbackExecutor the executor to call the listener on. */ fun setTaskRegionListener(listener: Consumer<Region>, callbackExecutor: Executor) { - desktopModeTaskRepository.setExclusionRegionListener(listener, callbackExecutor) + taskRepository.setExclusionRegionListener(listener, callbackExecutor) } + // TODO(b/358114479): Move this implementation into a separate class. override fun onUnhandledDrag( launchIntent: PendingIntent, - dragSurface: SurfaceControl, + dragEvent: DragEvent, onFinishCallback: Consumer<Boolean> ): Boolean { // TODO(b/320797628): Pass through which display we are dropping onto - val activeTasks = desktopModeTaskRepository.getActiveTasks(DEFAULT_DISPLAY) - if (!activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) { + if (!isDesktopModeShowing(DEFAULT_DISPLAY)) { // Not currently in desktop mode, ignore the drop return false } - val launchComponent = getComponent(launchIntent) if (!multiInstanceHelper.supportsMultiInstanceSplit(launchComponent)) { // TODO(b/320797628): Should only return early if there is an existing running task, and // notify the user as well. But for now, just ignore the drop. - KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "Dropped intent does not support multi-instance") + logV("Dropped intent does not support multi-instance") return false } - + val taskInfo = getFocusedFreeformTask(DEFAULT_DISPLAY) ?: return false + // TODO(b/358114479): Update drag and drop handling to give us visibility into when another + // window will accept a drag event. This way, we can hide the indicator when we won't + // be handling the transition here, allowing us to display the indicator accurately. + // For now, we create the indicator only on drag end and immediately dispose it. + val indicatorType = updateVisualIndicator(taskInfo, dragEvent.dragSurface, + dragEvent.x, dragEvent.y, + DragStartState.DRAGGED_INTENT) + releaseVisualIndicator() + val windowingMode = when (indicatorType) { + IndicatorType.TO_FULLSCREEN_INDICATOR -> { + WINDOWING_MODE_FULLSCREEN + } + IndicatorType.TO_SPLIT_LEFT_INDICATOR, + IndicatorType.TO_SPLIT_RIGHT_INDICATOR, + IndicatorType.TO_DESKTOP_INDICATOR + -> { + WINDOWING_MODE_FREEFORM + } + else -> error("Invalid indicator type: $indicatorType") + } + val displayLayout = displayController.getDisplayLayout(DEFAULT_DISPLAY) ?: return false + val newWindowBounds = Rect() + when (indicatorType) { + IndicatorType.TO_DESKTOP_INDICATOR -> { + // Use default bounds, but with the top-center at the drop point. + newWindowBounds.set(getDefaultDesktopTaskBounds(displayLayout)) + newWindowBounds.offsetTo( + dragEvent.x.toInt() - (newWindowBounds.width() / 2), + dragEvent.y.toInt() + ) + } + IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { + newWindowBounds.set(getSnapBounds(taskInfo, SnapPosition.RIGHT)) + } + IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { + newWindowBounds.set(getSnapBounds(taskInfo, SnapPosition.LEFT)) + } + else -> { + // Use empty bounds for the fullscreen case. + } + } // Start a new transition to launch the app val opts = ActivityOptions.makeBasic().apply { - launchWindowingMode = WINDOWING_MODE_FREEFORM + launchWindowingMode = windowingMode + launchBounds = newWindowBounds + pendingIntentBackgroundActivityStartMode = + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS pendingIntentLaunchFlags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK - setPendingIntentBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED - ) - isPendingIntentBackgroundActivityLaunchAllowedByPermission = true } + if (windowingMode == WINDOWING_MODE_FULLSCREEN) { + dragAndDropFullscreenCookie = Binder() + opts.launchCookie = dragAndDropFullscreenCookie + } val wct = WindowContainerTransaction() wct.sendPendingIntent(launchIntent, null, opts.toBundle()) - transitions.startTransition(TRANSIT_OPEN, wct, null /* handler */) + if (windowingMode == WINDOWING_MODE_FREEFORM) { + desktopModeDragAndDropTransitionHandler.handleDropEvent(wct) + } else { + transitions.startTransition(TRANSIT_OPEN, wct, null) + } // Report that this is handled by the listener onFinishCallback.accept(true) @@ -1392,7 +1766,7 @@ class DesktopTasksController( // We've assumed responsibility of cleaning up the drag surface, so do that now // TODO(b/320797628): Do an actual animation here for the drag surface val t = SurfaceControl.Transaction() - t.remove(dragSurface) + t.remove(dragEvent.dragSurface) t.apply() return true } @@ -1400,7 +1774,8 @@ class DesktopTasksController( private fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("${prefix}DesktopTasksController") - desktopModeTaskRepository.dump(pw, innerPrefix) + DesktopModeStatus.dump(pw, innerPrefix, context) + taskRepository.dump(pw, innerPrefix) } /** The interface for calls from outside the shell, within the host process. */ @@ -1458,7 +1833,7 @@ class DesktopTasksController( private val listener: VisibleTasksListener = object : VisibleTasksListener { override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) { - KtProtoLog.v( + ProtoLog.v( WM_SHELL_DESKTOP_MODE, "IDesktopModeImpl: onVisibilityChanged display=%d visible=%d", displayId, @@ -1470,17 +1845,39 @@ class DesktopTasksController( } } + private val mTaskbarDesktopTaskListener: TaskbarDesktopTaskListener = + object : TaskbarDesktopTaskListener { + override fun onTaskbarCornerRoundingUpdate( + hasTasksRequiringTaskbarRounding: Boolean) { + ProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "IDesktopModeImpl: onTaskbarCornerRoundingUpdate " + + "doesAnyTaskRequireTaskbarRounding=%s", + hasTasksRequiringTaskbarRounding + ) + + remoteListener.call { l -> + l.onTaskbarCornerRoundingUpdate(hasTasksRequiringTaskbarRounding) + } + } + } + init { remoteListener = SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>( controller, { c -> - c.desktopModeTaskRepository.addVisibleTasksListener( - listener, - c.mainExecutor - ) + run { + c.taskRepository.addVisibleTasksListener(listener, c.mainExecutor) + c.taskbarDesktopTaskListener = mTaskbarDesktopTaskListener + } }, - { c -> c.desktopModeTaskRepository.removeVisibleTasksListener(listener) } + { c -> + run { + c.taskRepository.removeVisibleTasksListener(listener) + c.taskbarDesktopTaskListener = null + } + } ) } @@ -1503,22 +1900,20 @@ class DesktopTasksController( } override fun stashDesktopApps(displayId: Int) { - KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "IDesktopModeImpl: stashDesktopApps is deprecated") + ProtoLog.w(WM_SHELL_DESKTOP_MODE, "IDesktopModeImpl: stashDesktopApps is deprecated") } override fun hideStashedDesktopApps(displayId: Int) { - KtProtoLog.w( - WM_SHELL_DESKTOP_MODE, - "IDesktopModeImpl: hideStashedDesktopApps is deprecated" - ) + ProtoLog.w(WM_SHELL_DESKTOP_MODE, + "IDesktopModeImpl: hideStashedDesktopApps is deprecated") } override fun getVisibleTaskCount(displayId: Int): Int { val result = IntArray(1) executeRemoteCallWithTaskPermission( controller, - "getVisibleTaskCount", - { controller -> result[0] = controller.getVisibleTaskCount(displayId) }, + "visibleTaskCount", + { controller -> result[0] = controller.visibleTaskCount(displayId) }, true /* blocking */ ) return result[0] @@ -1534,27 +1929,45 @@ class DesktopTasksController( } override fun setTaskListener(listener: IDesktopTaskListener?) { - KtProtoLog.v( - WM_SHELL_DESKTOP_MODE, - "IDesktopModeImpl: set task listener=%s", - listener ?: "null" - ) + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "IDesktopModeImpl: set task listener=%s", listener) executeRemoteCallWithTaskPermission(controller, "setTaskListener") { _ -> listener?.let { remoteListener.register(it) } ?: remoteListener.unregister() } } override fun moveToDesktop(taskId: Int, transitionSource: DesktopModeTransitionSource) { - executeRemoteCallWithTaskPermission(controller, "moveToDesktop") { c -> - c.moveToDesktop(taskId, transitionSource = transitionSource) + executeRemoteCallWithTaskPermission(controller, "moveTaskToDesktop") { c -> + c.moveTaskToDesktop(taskId, transitionSource = transitionSource) } } } + private fun logV(msg: String, vararg arguments: Any?) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + private fun logD(msg: String, vararg arguments: Any?) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + private fun logW(msg: String, vararg arguments: Any?) { + ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + companion object { @JvmField val DESKTOP_MODE_INITIAL_BOUNDS_SCALE = SystemProperties.getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f + + private const val TAG = "DesktopTasksController" + } + + /** Defines interface for classes that can listen to changes for task resize. */ + // TODO(b/343931111): Migrate to using TransitionObservers when ready + interface TaskbarDesktopTaskListener { + /** + * [hasTasksRequiringTaskbarRounding] is true when a task is either maximized or snapped + * left/right and rounded corners are enabled. + */ + fun onTaskbarCornerRoundingUpdate(hasTasksRequiringTaskbarRounding: Boolean) } /** The positions on a screen that a task can snap to. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt index 0f88384ec2ac..d84349b1ce1f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -17,72 +17,98 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RunningTaskInfo +import android.content.Context +import android.os.Handler import android.os.IBinder import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_TO_BACK import android.window.TransitionInfo import android.window.WindowContainerTransaction import androidx.annotation.VisibleForTesting +import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_MINIMIZE_WINDOW +import com.android.internal.jank.InteractionJankMonitor +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.protolog.ShellProtoLogGroup -import com.android.wm.shell.shared.DesktopModeStatus +import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TransitionObserver -import com.android.wm.shell.util.KtProtoLog /** * Limits the number of tasks shown in Desktop Mode. * * This class should only be used if - * [com.android.window.flags.Flags.enableDesktopWindowingTaskLimit()] is true. + * [android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT] + * is enabled and [maxTasksLimit] is strictly greater than 0. */ class DesktopTasksLimiter ( transitions: Transitions, private val taskRepository: DesktopModeTaskRepository, private val shellTaskOrganizer: ShellTaskOrganizer, + private val maxTasksLimit: Int, + private val interactionJankMonitor: InteractionJankMonitor, + private val context: Context, + @ShellMainThread private val handler: Handler, ) { private val minimizeTransitionObserver = MinimizeTransitionObserver() + @VisibleForTesting + val leftoverMinimizedTasksRemover = LeftoverMinimizedTasksRemover() init { + require(maxTasksLimit > 0) { + "DesktopTasksLimiter should not be created with a maxTasksLimit at 0 or less. " + + "Current value: $maxTasksLimit." + } transitions.registerObserver(minimizeTransitionObserver) + taskRepository.addActiveTaskListener(leftoverMinimizedTasksRemover) + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: starting limiter with a maximum of %d tasks", maxTasksLimit) } - private data class TaskDetails (val displayId: Int, val taskId: Int) + private data class TaskDetails( + val displayId: Int, + val taskId: Int, + var transitionInfo: TransitionInfo? + ) // TODO(b/333018485): replace this observer when implementing the minimize-animation private inner class MinimizeTransitionObserver : TransitionObserver { - private val mPendingTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>() + private val pendingTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>() + private val activeTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>() fun addPendingTransitionToken(transition: IBinder, taskDetails: TaskDetails) { - mPendingTransitionTokensAndTasks[transition] = taskDetails + pendingTransitionTokensAndTasks[transition] = taskDetails } override fun onTransitionReady( - transition: IBinder, - info: TransitionInfo, - startTransaction: SurfaceControl.Transaction, - finishTransaction: SurfaceControl.Transaction + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction ) { - val taskToMinimize = mPendingTransitionTokensAndTasks.remove(transition) ?: return + val taskToMinimize = pendingTransitionTokensAndTasks.remove(transition) ?: return if (!taskRepository.isActiveTask(taskToMinimize.taskId)) return if (!isTaskReorderedToBackOrInvisible(info, taskToMinimize)) { - KtProtoLog.v( + ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DesktopTasksLimiter: task %d is not reordered to back nor invis", taskToMinimize.taskId) return } + + taskToMinimize.transitionInfo = info + activeTransitionTokensAndTasks[transition] = taskToMinimize this@DesktopTasksLimiter.markTaskMinimized( taskToMinimize.displayId, taskToMinimize.taskId) } /** - * Returns whether the given Task is being reordered to the back in the given transition, or - * is already invisible. + * Returns whether the Task [taskDetails] is being reordered to the back in the transition + * [info], or is already invisible. * - * <p> This check can be used to double-check that a task was indeed minimized before + * This check can be used to double-check that a task was indeed minimized before * marking it as such. */ private fun isTaskReorderedToBackOrInvisible( @@ -97,28 +123,77 @@ class DesktopTasksLimiter ( return taskChange.mode == TRANSIT_TO_BACK } - override fun onTransitionStarting(transition: IBinder) {} + override fun onTransitionStarting(transition: IBinder) { + val mActiveTaskDetails = activeTransitionTokensAndTasks[transition] + if (mActiveTaskDetails != null && mActiveTaskDetails.transitionInfo != null) { + // Begin minimize window CUJ instrumentation. + interactionJankMonitor.begin( + mActiveTaskDetails.transitionInfo?.rootLeash, context, handler, + CUJ_DESKTOP_MODE_MINIMIZE_WINDOW + ) + } + } override fun onTransitionMerged(merged: IBinder, playing: IBinder) { - mPendingTransitionTokensAndTasks.remove(merged)?.let { taskToTransfer -> - mPendingTransitionTokensAndTasks[playing] = taskToTransfer + if (activeTransitionTokensAndTasks.remove(merged) != null) { + interactionJankMonitor.end(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW) + } + pendingTransitionTokensAndTasks.remove(merged)?.let { taskToTransfer -> + pendingTransitionTokensAndTasks[playing] = taskToTransfer } } override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { - KtProtoLog.v( + ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DesktopTasksLimiter: transition %s finished", transition) - mPendingTransitionTokensAndTasks.remove(transition) + if (activeTransitionTokensAndTasks.remove(transition) != null) { + if (aborted) { + interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW) + } else { + interactionJankMonitor.end(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW) + } + } + pendingTransitionTokensAndTasks.remove(transition) + } + } + + @VisibleForTesting + inner class LeftoverMinimizedTasksRemover : DesktopModeTaskRepository.ActiveTasksListener { + override fun onActiveTasksChanged(displayId: Int) { + val wct = WindowContainerTransaction() + removeLeftoverMinimizedTasks(displayId, wct) + shellTaskOrganizer.applyTransaction(wct) + } + + fun removeLeftoverMinimizedTasks(displayId: Int, wct: WindowContainerTransaction) { + if (taskRepository.getActiveNonMinimizedOrderedTasks(displayId).isNotEmpty()) { + return + } + val remainingMinimizedTasks = taskRepository.getMinimizedTasks(displayId) + if (remainingMinimizedTasks.isEmpty()) { + return + } + ProtoLog.v( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: removing leftover minimized tasks: %s", + remainingMinimizedTasks, + ) + remainingMinimizedTasks.forEach { taskIdToRemove -> + val taskToRemove = shellTaskOrganizer.getRunningTaskInfo(taskIdToRemove) + if (taskToRemove != null) { + wct.removeTask(taskToRemove.token) + } + } } } /** - * Mark a task as minimized, this should only be done after the corresponding transition has - * finished so we don't minimize the task if the transition fails. + * Mark [taskId], which must be on [displayId], as minimized, this should only be done after the + * corresponding transition has finished so we don't minimize the task if the transition fails. */ private fun markTaskMinimized(displayId: Int, taskId: Int) { - KtProtoLog.v( + ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DesktopTasksLimiter: marking %d as minimized", taskId) taskRepository.minimizeTask(displayId, taskId) @@ -126,23 +201,21 @@ class DesktopTasksLimiter ( /** * Add a minimize-transition to [wct] if adding [newFrontTaskInfo] brings us over the task - * limit. + * limit, returning the task to minimize. * - * @param transition the transition that the minimize-transition will be appended to, or null if - * the transition will be started later. - * @return the ID of the minimized task, or null if no task is being minimized. + * The task must be on [displayId]. */ fun addAndGetMinimizeTaskChangesIfNeeded( displayId: Int, wct: WindowContainerTransaction, newFrontTaskInfo: RunningTaskInfo, ): RunningTaskInfo? { - KtProtoLog.v( + ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DesktopTasksLimiter: addMinimizeBackTaskChangesIfNeeded, newFrontTask=%d", newFrontTaskInfo.taskId) val newTaskListOrderedFrontToBack = createOrderedTaskListWithGivenTaskInFront( - taskRepository.getActiveNonMinimizedTasksOrderedFrontToBack(displayId), + taskRepository.getActiveNonMinimizedOrderedTasks(displayId), newFrontTaskInfo.taskId) val taskToMinimize = getTaskToMinimizeIfNeeded(newTaskListOrderedFrontToBack) if (taskToMinimize != null) { @@ -158,16 +231,10 @@ class DesktopTasksLimiter ( */ fun addPendingMinimizeChange(transition: IBinder, displayId: Int, taskId: Int) { minimizeTransitionObserver.addPendingTransitionToken( - transition, TaskDetails(displayId, taskId)) + transition, TaskDetails(displayId, taskId, transitionInfo = null)) } /** - * Returns the maximum number of tasks that should ever be displayed at the same time in Desktop - * Mode. - */ - fun getMaxTaskLimit(): Int = DesktopModeStatus.getMaxTaskLimit() - - /** * Returns the Task to minimize given 1. a list of visible tasks ordered from front to back and * 2. a new task placed in front of all the others. */ @@ -184,20 +251,22 @@ class DesktopTasksLimiter ( fun getTaskToMinimizeIfNeeded( visibleFreeformTaskIdsOrderedFrontToBack: List<Int> ): RunningTaskInfo? { - if (visibleFreeformTaskIdsOrderedFrontToBack.size <= getMaxTaskLimit()) { - KtProtoLog.v( + if (visibleFreeformTaskIdsOrderedFrontToBack.size <= maxTasksLimit) { + ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DesktopTasksLimiter: no need to minimize; tasks below limit") // No need to minimize anything return null } + val taskIdToMinimize = visibleFreeformTaskIdsOrderedFrontToBack.last() val taskToMinimize = - shellTaskOrganizer.getRunningTaskInfo( - visibleFreeformTaskIdsOrderedFrontToBack.last()) + shellTaskOrganizer.getRunningTaskInfo(taskIdToMinimize) if (taskToMinimize == null) { - KtProtoLog.e( + ProtoLog.e( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "DesktopTasksLimiter: taskToMinimize == null") + "DesktopTasksLimiter: taskToMinimize(taskId = %d) == null", + taskIdToMinimize, + ) return null } return taskToMinimize @@ -212,7 +281,5 @@ class DesktopTasksLimiter ( } @VisibleForTesting - fun getTransitionObserver(): TransitionObserver { - return minimizeTransitionObserver - } + fun getTransitionObserver(): TransitionObserver = minimizeTransitionObserver }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index dae75f90e3ae..0841628853a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -21,35 +21,38 @@ import android.os.IBinder import android.view.SurfaceControl import android.view.WindowManager import android.window.TransitionInfo -import com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity +import android.window.WindowContainerTransaction +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE -import com.android.wm.shell.shared.DesktopModeStatus +import android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions -import com.android.wm.shell.util.KtProtoLog /** - * A [Transitions.TransitionObserver] that observes shell transitions and updates - * the [DesktopModeTaskRepository] state TODO: b/332682201 - * This observes transitions related to desktop mode - * and other transitions that originate both within and outside shell. + * A [Transitions.TransitionObserver] that observes shell transitions and updates the + * [DesktopModeTaskRepository] state TODO: b/332682201 This observes transitions related to desktop + * mode and other transitions that originate both within and outside shell. */ class DesktopTasksTransitionObserver( - context: Context, + private val context: Context, private val desktopModeTaskRepository: DesktopModeTaskRepository, private val transitions: Transitions, + private val shellTaskOrganizer: ShellTaskOrganizer, shellInit: ShellInit ) : Transitions.TransitionObserver { init { - if (Transitions.ENABLE_SHELL_TRANSITIONS && - DesktopModeStatus.canEnterDesktopMode(context)) { + if ( + Transitions.ENABLE_SHELL_TRANSITIONS && DesktopModeStatus.canEnterDesktopMode(context) + ) { shellInit.addInitCallback(::onInit, this) } } fun onInit() { - KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopTasksTransitionObserver: onInit") + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopTasksTransitionObserver: onInit") transitions.registerObserver(this) } @@ -76,15 +79,23 @@ class DesktopTasksTransitionObserver( } private fun updateWallpaperToken(info: TransitionInfo) { - if (!enableDesktopWindowingWallpaperActivity()) { + if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()) { return } info.changes.forEach { change -> change.taskInfo?.let { taskInfo -> if (DesktopWallpaperActivity.isWallpaperTask(taskInfo)) { when (change.mode) { - WindowManager.TRANSIT_OPEN -> + WindowManager.TRANSIT_OPEN -> { desktopModeTaskRepository.wallpaperActivityToken = taskInfo.token + // After the task for the wallpaper is created, set it non-trimmable. + // This is important to prevent recents from trimming and removing the + // task. + shellTaskOrganizer.applyTransaction( + WindowContainerTransaction() + .setTaskTrimmableFromRecents(taskInfo.token, false) + ) + } WindowManager.TRANSIT_CLOSE -> desktopModeTaskRepository.wallpaperActivityToken = null else -> {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt index c4a4474689fa..1c2415c236ad 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt @@ -21,8 +21,8 @@ import android.app.ActivityManager import android.content.ComponentName import android.os.Bundle import android.view.WindowManager +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE -import com.android.wm.shell.util.KtProtoLog /** * A transparent activity used in the desktop mode to show the wallpaper under the freeform windows. @@ -36,7 +36,7 @@ import com.android.wm.shell.util.KtProtoLog class DesktopWallpaperActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { - KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopWallpaperActivity: onCreate") + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopWallpaperActivity: onCreate") super.onCreate(savedInstanceState) window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index d99b724c936f..2bc01b2f310e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -22,62 +22,63 @@ import android.graphics.Rect import android.os.Bundle import android.os.IBinder import android.os.SystemClock +import android.os.SystemProperties import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CLOSE import android.window.TransitionInfo import android.window.TransitionInfo.Change import android.window.TransitionRequestInfo -import android.window.WindowContainerToken import android.window.WindowContainerTransaction +import com.android.internal.annotations.VisibleForTesting +import com.android.internal.dynamicanimation.animation.SpringForce +import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD +import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE +import com.android.internal.jank.InteractionJankMonitor +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.RootTaskDisplayAreaOrganizer -import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT -import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT -import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED -import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition +import com.android.wm.shell.animation.FloatProperties import com.android.wm.shell.protolog.ShellProtoLogGroup import com.android.wm.shell.shared.TransitionUtil +import com.android.wm.shell.shared.animation.PhysicsAnimator +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED +import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP import com.android.wm.shell.transition.Transitions.TransitionHandler -import com.android.wm.shell.util.KtProtoLog import com.android.wm.shell.windowdecor.MoveToDesktopAnimator import com.android.wm.shell.windowdecor.MoveToDesktopAnimator.Companion.DRAG_FREEFORM_SCALE import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener import java.util.function.Supplier +import kotlin.math.max /** * Handles the transition to enter desktop from fullscreen by dragging on the handle bar. It also * handles the cancellation case where the task is dragged back to the status bar area in the same * gesture. + * + * It's a base sealed class that delegates flag dependant logic to its subclasses: + * [DefaultDragToDesktopTransitionHandler] and [SpringDragToDesktopTransitionHandler] + * + * TODO(b/356764679): Clean up after the full flag rollout */ -class DragToDesktopTransitionHandler( +sealed class DragToDesktopTransitionHandler( private val context: Context, private val transitions: Transitions, private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, - private val transactionSupplier: Supplier<SurfaceControl.Transaction> + protected val interactionJankMonitor: InteractionJankMonitor, + protected val transactionSupplier: Supplier<SurfaceControl.Transaction>, ) : TransitionHandler { - constructor( - context: Context, - transitions: Transitions, - rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer - ) : this( - context, - transitions, - rootTaskDisplayAreaOrganizer, - Supplier { SurfaceControl.Transaction() } - ) - - private val rectEvaluator = RectEvaluator(Rect()) + protected val rectEvaluator = RectEvaluator(Rect()) private val launchHomeIntent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME) - private var dragToDesktopStateListener: DragToDesktopStateListener? = null private lateinit var splitScreenController: SplitScreenController private var transitionState: TransitionState? = null - private lateinit var onTaskResizeAnimationListener: OnTaskResizeAnimationListener /** Whether a drag-to-desktop transition is in progress. */ val inProgress: Boolean @@ -86,20 +87,18 @@ class DragToDesktopTransitionHandler( /** The task id of the task currently being dragged from fullscreen/split. */ val draggingTaskId: Int get() = transitionState?.draggedTaskId ?: INVALID_TASK_ID - /** Sets a listener to receive callback about events during the transition animation. */ - fun setDragToDesktopStateListener(listener: DragToDesktopStateListener) { - dragToDesktopStateListener = listener - } + + /** Listener to receive callback about events during the transition animation. */ + var dragToDesktopStateListener: DragToDesktopStateListener? = null + + /** Task listener for animation start, task bounds resize, and the animation finish */ + lateinit var onTaskResizeAnimationListener: OnTaskResizeAnimationListener /** Setter needed to avoid cyclic dependency. */ fun setSplitScreenController(controller: SplitScreenController) { splitScreenController = controller } - fun setOnTaskResizeAnimatorListener(listener: OnTaskResizeAnimationListener) { - onTaskResizeAnimationListener = listener - } - /** * Starts a transition that performs a transient launch of Home so that Home is brought to the * front while still keeping the currently focused task that is being dragged resumed. This @@ -114,7 +113,7 @@ class DragToDesktopTransitionHandler( dragToDesktopAnimator: MoveToDesktopAnimator, ) { if (inProgress) { - KtProtoLog.v( + ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DragToDesktop: Drag to desktop transition already in progress." ) @@ -214,16 +213,18 @@ class DragToDesktopTransitionHandler( startCancelAnimation() } else if ( state.draggedTaskChange != null && - (cancelState == CancelState.CANCEL_SPLIT_LEFT || + (cancelState == CancelState.CANCEL_SPLIT_LEFT || cancelState == CancelState.CANCEL_SPLIT_RIGHT) - ) { + ) { // We have a valid dragged task, but the animation will be handled by // SplitScreenController; request the transition here. - @SplitPosition val splitPosition = if (cancelState == CancelState.CANCEL_SPLIT_LEFT) { - SPLIT_POSITION_TOP_OR_LEFT - } else { - SPLIT_POSITION_BOTTOM_OR_RIGHT - } + @SplitPosition + val splitPosition = + if (cancelState == CancelState.CANCEL_SPLIT_LEFT) { + SPLIT_POSITION_TOP_OR_LEFT + } else { + SPLIT_POSITION_BOTTOM_OR_RIGHT + } val wct = WindowContainerTransaction() restoreWindowOrder(wct, state) state.startTransitionFinishTransaction?.apply() @@ -246,20 +247,20 @@ class DragToDesktopTransitionHandler( wct: WindowContainerTransaction ) { val state = requireTransitionState() - val taskInfo = state.draggedTaskChange?.taskInfo - ?: error("Expected non-null taskInfo") + val taskInfo = state.draggedTaskChange?.taskInfo ?: error("Expected non-null taskInfo") val taskBounds = Rect(taskInfo.configuration.windowConfiguration.bounds) val taskScale = state.dragAnimator.scale val scaledWidth = taskBounds.width() * taskScale val scaledHeight = taskBounds.height() * taskScale val dragPosition = PointF(state.dragAnimator.position) state.dragAnimator.cancelAnimator() - val animatedTaskBounds = Rect( - dragPosition.x.toInt(), - dragPosition.y.toInt(), - (dragPosition.x + scaledWidth).toInt(), - (dragPosition.y + scaledHeight).toInt() - ) + val animatedTaskBounds = + Rect( + dragPosition.x.toInt(), + dragPosition.y.toInt(), + (dragPosition.x + scaledWidth).toInt(), + (dragPosition.y + scaledHeight).toInt() + ) requestSplitSelect(wct, taskInfo, splitPosition, animatedTaskBounds) } @@ -280,12 +281,7 @@ class DragToDesktopTransitionHandler( } wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW) wct.setDensityDpi(taskInfo.token, context.resources.displayMetrics.densityDpi) - splitScreenController.requestEnterSplitSelect( - taskInfo, - wct, - splitPosition, - taskBounds - ) + splitScreenController.requestEnterSplitSelect(taskInfo, wct, splitPosition, taskBounds) } override fun startAnimation( @@ -304,24 +300,18 @@ class DragToDesktopTransitionHandler( return false } - // Layering: non-wallpaper, non-home tasks excluding the dragged task go at the bottom, - // then Home on top of that, wallpaper on top of that and finally the dragged task on top - // of everything. - val appLayers = info.changes.size - val homeLayers = info.changes.size * 2 - val wallpaperLayers = info.changes.size * 3 - val dragLayer = wallpaperLayers + val layers = calculateStartDragToDesktopLayers(info) val leafTaskFilter = TransitionUtil.LeafTaskFilter() info.changes.withIndex().forEach { (i, change) -> if (TransitionUtil.isWallpaper(change)) { - val layer = wallpaperLayers - i + val layer = layers.topWallpaperLayer - i startTransaction.apply { setLayer(change.leash, layer) show(change.leash) } } else if (isHomeChange(change)) { - state.homeToken = change.container - val layer = homeLayers - i + state.homeChange = change + val layer = layers.topHomeLayer - i startTransaction.apply { setLayer(change.leash, layer) show(change.leash) @@ -335,11 +325,11 @@ class DragToDesktopTransitionHandler( if (state.cancelState == CancelState.NO_CANCEL) { // Normal case, split root goes to the bottom behind everything // else. - appLayers - i + layers.topAppLayer - i } else { // Cancel-early case, pretend nothing happened so split root stays // top. - dragLayer + layers.dragLayer } startTransaction.apply { setLayer(change.leash, layer) @@ -354,7 +344,7 @@ class DragToDesktopTransitionHandler( state.draggedTaskChange = change val bounds = change.endAbsBounds startTransaction.apply { - setLayer(change.leash, dragLayer) + setLayer(change.leash, layers.dragLayer) setWindowCrop(change.leash, bounds.width(), bounds.height()) show(change.leash) } @@ -367,7 +357,7 @@ class DragToDesktopTransitionHandler( state.otherRootChanges.add(change) val bounds = change.endAbsBounds startTransaction.apply { - setLayer(change.leash, appLayers - i) + setLayer(change.leash, layers.topAppLayer - i) setWindowCrop(change.leash, bounds.width(), bounds.height()) show(change.leash) } @@ -384,7 +374,7 @@ class DragToDesktopTransitionHandler( // occurred. if ( change.taskInfo?.taskId == state.draggedTaskId && - state.cancelState != CancelState.STANDARD_CANCEL + state.cancelState != CancelState.STANDARD_CANCEL ) { // We need access to the dragged task's change in both non-cancel and split // cancel cases. @@ -392,8 +382,8 @@ class DragToDesktopTransitionHandler( } if ( change.taskInfo?.taskId == state.draggedTaskId && - state.cancelState == CancelState.NO_CANCEL - ) { + state.cancelState == CancelState.NO_CANCEL + ) { taskDisplayAreaOrganizer.reparentToDisplayArea( change.endDisplayId, change.leash, @@ -401,13 +391,14 @@ class DragToDesktopTransitionHandler( ) val bounds = change.endAbsBounds startTransaction.apply { - setLayer(change.leash, dragLayer) + setLayer(change.leash, layers.dragLayer) setWindowCrop(change.leash, bounds.width(), bounds.height()) show(change.leash) } } } } + state.surfaceLayers = layers state.startTransitionFinishCb = finishCallback state.startTransitionFinishTransaction = finishTransaction startTransaction.apply() @@ -426,19 +417,20 @@ class DragToDesktopTransitionHandler( startCancelDragToDesktopTransition() } else if ( state.cancelState == CancelState.CANCEL_SPLIT_LEFT || - state.cancelState == CancelState.CANCEL_SPLIT_RIGHT - ){ + state.cancelState == CancelState.CANCEL_SPLIT_RIGHT + ) { // Cancel-early case for split-cancel. The state was flagged already as a cancel for // requesting split select. Similar to the above, this can happen due to quick fling // gestures. We can simply request split here without needing to calculate animated // task bounds as the task has not shrunk at all. - val splitPosition = if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT) { - SPLIT_POSITION_TOP_OR_LEFT - } else { - SPLIT_POSITION_BOTTOM_OR_RIGHT - } - val taskInfo = state.draggedTaskChange?.taskInfo - ?: error("Expected non-null task info.") + val splitPosition = + if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT) { + SPLIT_POSITION_TOP_OR_LEFT + } else { + SPLIT_POSITION_BOTTOM_OR_RIGHT + } + val taskInfo = + state.draggedTaskChange?.taskInfo ?: error("Expected non-null task info.") val wct = WindowContainerTransaction() restoreWindowOrder(wct) state.startTransitionFinishTransaction?.apply() @@ -448,6 +440,15 @@ class DragToDesktopTransitionHandler( return true } + /** + * Calculates start drag to desktop layers for transition [info]. The leash layer is calculated + * based on its change position in the transition, e.g. `appLayer = appLayers - i`, where i is + * the change index. + */ + protected abstract fun calculateStartDragToDesktopLayers( + info: TransitionInfo + ): DragToDesktopLayers + override fun mergeAnimation( transition: IBinder, info: TransitionInfo, @@ -457,8 +458,10 @@ class DragToDesktopTransitionHandler( ) { val state = requireTransitionState() // We don't want to merge the split select animation if that's what we requested. - if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT || - state.cancelState == CancelState.CANCEL_SPLIT_RIGHT) { + if ( + state.cancelState == CancelState.CANCEL_SPLIT_LEFT || + state.cancelState == CancelState.CANCEL_SPLIT_RIGHT + ) { clearState() return } @@ -477,111 +480,144 @@ class DragToDesktopTransitionHandler( state.startTransitionFinishCb ?: error("Start transition expected to be waiting for merge but wasn't") if (isEndTransition) { - info.changes.withIndex().forEach { (i, change) -> - // If we're exiting split, hide the remaining split task. - if ( - state is TransitionState.FromSplit && - change.taskInfo?.taskId == state.otherSplitTask - ) { - t.hide(change.leash) - startTransactionFinishT.hide(change.leash) + setupEndDragToDesktop( + info, + startTransaction = t, + finishTransaction = startTransactionFinishT + ) + // Call finishCallback to merge animation before startTransitionFinishCb is called + finishCallback.onTransitionFinished(null /* wct */) + animateEndDragToDesktop(startTransaction = t, startTransitionFinishCb) + } else if (isCancelTransition) { + info.changes.forEach { change -> + t.show(change.leash) + startTransactionFinishT.show(change.leash) + } + t.apply() + finishCallback.onTransitionFinished(null /* wct */) + startTransitionFinishCb.onTransitionFinished(null /* wct */) + clearState() + } + } + + protected open fun setupEndDragToDesktop( + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction + ) { + val state = requireTransitionState() + val freeformTaskChanges = mutableListOf<Change>() + info.changes.forEachIndexed { i, change -> + when { + state is TransitionState.FromSplit && + change.taskInfo?.taskId == state.otherSplitTask -> { + // If we're exiting split, hide the remaining split task. + startTransaction.hide(change.leash) + finishTransaction.hide(change.leash) } - if (change.mode == TRANSIT_CLOSE) { - t.hide(change.leash) - startTransactionFinishT.hide(change.leash) - } else if (change.taskInfo?.taskId == state.draggedTaskId) { - t.show(change.leash) - startTransactionFinishT.show(change.leash) + change.mode == TRANSIT_CLOSE -> { + startTransaction.hide(change.leash) + finishTransaction.hide(change.leash) + } + change.taskInfo?.taskId == state.draggedTaskId -> { + startTransaction.show(change.leash) + finishTransaction.show(change.leash) state.draggedTaskChange = change - } else if (change.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM) { + // Restoring the dragged leash layer as it gets reset in the merge transition + state.surfaceLayers?.let { + startTransaction.setLayer(change.leash, it.dragLayer) + } + } + change.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM -> { // Other freeform tasks that are being restored go behind the dragged task. val draggedTaskLeash = state.draggedTaskChange?.leash ?: error("Expected dragged leash to be non-null") - t.setRelativeLayer(change.leash, draggedTaskLeash, -i) - startTransactionFinishT.setRelativeLayer(change.leash, draggedTaskLeash, -i) + startTransaction.setRelativeLayer(change.leash, draggedTaskLeash, -i) + finishTransaction.setRelativeLayer(change.leash, draggedTaskLeash, -i) + freeformTaskChanges.add(change) } } + } - val draggedTaskChange = - state.draggedTaskChange - ?: throw IllegalStateException("Expected non-null change of dragged task") - val draggedTaskLeash = draggedTaskChange.leash - val startBounds = draggedTaskChange.startAbsBounds - val endBounds = draggedTaskChange.endAbsBounds - - // Pause any animation that may be currently playing; we will use the relevant - // details of that animation here. - state.dragAnimator.cancelAnimator() - // We still apply scale to task bounds; as we animate the bounds to their - // end value, animate scale to 1. - val startScale = state.dragAnimator.scale - val startPosition = state.dragAnimator.position - val unscaledStartWidth = startBounds.width() - val unscaledStartHeight = startBounds.height() - val unscaledStartBounds = - Rect( - startPosition.x.toInt(), - startPosition.y.toInt(), - startPosition.x.toInt() + unscaledStartWidth, - startPosition.y.toInt() + unscaledStartHeight - ) + state.freeformTaskChanges = freeformTaskChanges + } + + protected open fun animateEndDragToDesktop( + startTransaction: SurfaceControl.Transaction, + startTransitionFinishCb: Transitions.TransitionFinishCallback + ) { + val state = requireTransitionState() + val draggedTaskChange = + state.draggedTaskChange ?: error("Expected non-null change of dragged task") + val draggedTaskLeash = draggedTaskChange.leash + val startBounds = draggedTaskChange.startAbsBounds + val endBounds = draggedTaskChange.endAbsBounds - dragToDesktopStateListener?.onCommitToDesktopAnimationStart(t) - // Accept the merge by applying the merging transaction (applied by #showResizeVeil) - // and finish callback. Show the veil and position the task at the first frame before - // starting the final animation. - onTaskResizeAnimationListener.onAnimationStart( - state.draggedTaskId, - t, - unscaledStartBounds + // Cancel any animation that may be currently playing; we will use the relevant + // details of that animation here. + state.dragAnimator.cancelAnimator() + // We still apply scale to task bounds; as we animate the bounds to their + // end value, animate scale to 1. + val startScale = state.dragAnimator.scale + val startPosition = state.dragAnimator.position + val unscaledStartWidth = startBounds.width() + val unscaledStartHeight = startBounds.height() + val unscaledStartBounds = + Rect( + startPosition.x.toInt(), + startPosition.y.toInt(), + startPosition.x.toInt() + unscaledStartWidth, + startPosition.y.toInt() + unscaledStartHeight ) - finishCallback.onTransitionFinished(null /* wct */) - val tx: SurfaceControl.Transaction = transactionSupplier.get() - ValueAnimator.ofObject(rectEvaluator, unscaledStartBounds, endBounds) - .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS) - .apply { - addUpdateListener { animator -> - val animBounds = animator.animatedValue as Rect - val animFraction = animator.animatedFraction - // Progress scale from starting value to 1 as animation plays. - val animScale = startScale + animFraction * (1 - startScale) - tx.apply { - setScale(draggedTaskLeash, animScale, animScale) - setPosition( - draggedTaskLeash, - animBounds.left.toFloat(), - animBounds.top.toFloat() - ) - setWindowCrop(draggedTaskLeash, animBounds.width(), animBounds.height()) - } - onTaskResizeAnimationListener.onBoundsChange( - state.draggedTaskId, - tx, - animBounds + + dragToDesktopStateListener?.onCommitToDesktopAnimationStart(startTransaction) + // Accept the merge by applying the merging transaction (applied by #showResizeVeil) + // and finish callback. Show the veil and position the task at the first frame before + // starting the final animation. + onTaskResizeAnimationListener.onAnimationStart( + state.draggedTaskId, + startTransaction, + unscaledStartBounds + ) + val tx: SurfaceControl.Transaction = transactionSupplier.get() + ValueAnimator.ofObject(rectEvaluator, unscaledStartBounds, endBounds) + .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS) + .apply { + addUpdateListener { animator -> + val animBounds = animator.animatedValue as Rect + val animFraction = animator.animatedFraction + // Progress scale from starting value to 1 as animation plays. + val animScale = startScale + animFraction * (1 - startScale) + tx.apply { + setScale(draggedTaskLeash, animScale, animScale) + setPosition( + draggedTaskLeash, + animBounds.left.toFloat(), + animBounds.top.toFloat() ) + setWindowCrop(draggedTaskLeash, animBounds.width(), animBounds.height()) } - addListener( - object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - onTaskResizeAnimationListener.onAnimationEnd(state.draggedTaskId) - startTransitionFinishCb.onTransitionFinished(null /* null */) - clearState() - } - } + onTaskResizeAnimationListener.onBoundsChange( + state.draggedTaskId, + tx, + animBounds ) - start() } - } else if (isCancelTransition) { - info.changes.forEach { change -> - t.show(change.leash) - startTransactionFinishT.show(change.leash) + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + onTaskResizeAnimationListener.onAnimationEnd(state.draggedTaskId) + startTransitionFinishCb.onTransitionFinished(/* wct = */ null) + clearState() + interactionJankMonitor.end( + CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE + ) + } + } + ) + start() } - t.apply() - finishCallback.onTransitionFinished(null /* wct */) - startTransitionFinishCb.onTransitionFinished(null /* wct */) - clearState() - } } override fun handleRequest( @@ -598,17 +634,33 @@ class DragToDesktopTransitionHandler( finishTransaction: SurfaceControl.Transaction? ) { val state = transitionState ?: return - if (aborted && state.startTransitionToken == transition) { - KtProtoLog.v( + if (!aborted) { + return + } + if (state.startTransitionToken == transition) { + ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DragToDesktop: onTransitionConsumed() start transition aborted" ) state.startAborted = true + // The start-transition (DRAG_HOLD) is aborted, cancel its jank interaction. + interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD) + } else if (state.cancelTransitionToken != transition) { + // This transition being aborted is neither the start, nor the cancel transition, so + // it must be the finish transition (DRAG_RELEASE); cancel its jank interaction. + interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE) } } - private fun isHomeChange(change: Change): Boolean { - return change.taskInfo?.activityType == ACTIVITY_TYPE_HOME + /** Checks if the change is a home task change */ + @VisibleForTesting + fun isHomeChange(change: Change): Boolean { + return change.taskInfo?.let { + it.activityType == ACTIVITY_TYPE_HOME && + // Skip translucent wizard task with type home + // TODO(b/368334295): Remove when the multiple home changes issue is resolved + !(it.isTopActivityTransparent && it.numActivities == 1) + } ?: false } private fun startCancelAnimation() { @@ -661,9 +713,7 @@ class DragToDesktopTransitionHandler( val wct = WindowContainerTransaction() restoreWindowOrder(wct, state) state.cancelTransitionToken = - transitions.startTransition( - TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, wct, this - ) + transitions.startTransition(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, wct, this) } private fun restoreWindowOrder( @@ -696,11 +746,12 @@ class DragToDesktopTransitionHandler( wct.reorder(wc, true /* toTop */) } } - val homeWc = state.homeToken ?: error("Home task should be non-null before cancelling") + val homeWc = + state.homeChange?.container ?: error("Home task should be non-null before cancelling") wct.restoreTransientOrder(homeWc) } - private fun clearState() { + protected fun clearState() { transitionState = null } @@ -720,10 +771,27 @@ class DragToDesktopTransitionHandler( return splitScreenController.getTaskInfo(otherTaskPos)?.taskId } - private fun requireTransitionState(): TransitionState { + protected fun requireTransitionState(): TransitionState { return transitionState ?: error("Expected non-null transition state") } + /** + * Represents the layering (Z order) that will be given to any window based on its type during + * the "start" transition of the drag-to-desktop transition. + * + * @param topAppLayer Used to calculate the app layer z-order = `topAppLayer - changeIndex`. + * @param topHomeLayer Used to calculate the home layer z-order = `topHomeLayer - changeIndex`. + * @param topWallpaperLayer Used to calculate the wallpaper layer z-order = `topWallpaperLayer - + * changeIndex` + * @param dragLayer Defines the drag layer z-order + */ + data class DragToDesktopLayers( + val topAppLayer: Int, + val topHomeLayer: Int, + val topWallpaperLayer: Int, + val dragLayer: Int, + ) + interface DragToDesktopStateListener { fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction) @@ -737,8 +805,10 @@ class DragToDesktopTransitionHandler( abstract var startTransitionFinishCb: Transitions.TransitionFinishCallback? abstract var startTransitionFinishTransaction: SurfaceControl.Transaction? abstract var cancelTransitionToken: IBinder? - abstract var homeToken: WindowContainerToken? + abstract var homeChange: Change? abstract var draggedTaskChange: Change? + abstract var freeformTaskChanges: List<Change> + abstract var surfaceLayers: DragToDesktopLayers? abstract var cancelState: CancelState abstract var startAborted: Boolean @@ -749,8 +819,10 @@ class DragToDesktopTransitionHandler( override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null, override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null, override var cancelTransitionToken: IBinder? = null, - override var homeToken: WindowContainerToken? = null, + override var homeChange: Change? = null, override var draggedTaskChange: Change? = null, + override var freeformTaskChanges: List<Change> = emptyList(), + override var surfaceLayers: DragToDesktopLayers? = null, override var cancelState: CancelState = CancelState.NO_CANCEL, override var startAborted: Boolean = false, var otherRootChanges: MutableList<Change> = mutableListOf() @@ -763,8 +835,10 @@ class DragToDesktopTransitionHandler( override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null, override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null, override var cancelTransitionToken: IBinder? = null, - override var homeToken: WindowContainerToken? = null, + override var homeChange: Change? = null, override var draggedTaskChange: Change? = null, + override var freeformTaskChanges: List<Change> = emptyList(), + override var surfaceLayers: DragToDesktopLayers? = null, override var cancelState: CancelState = CancelState.NO_CANCEL, override var startAborted: Boolean = false, var splitRootChange: Change? = null, @@ -786,6 +860,263 @@ class DragToDesktopTransitionHandler( companion object { /** The duration of the animation to commit or cancel the drag-to-desktop gesture. */ - private const val DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS = 336L + internal const val DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS = 336L + } +} + +/** Enables flagged rollout of the [SpringDragToDesktopTransitionHandler] */ +class DefaultDragToDesktopTransitionHandler +@JvmOverloads +constructor( + context: Context, + transitions: Transitions, + taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + interactionJankMonitor: InteractionJankMonitor, + transactionSupplier: Supplier<SurfaceControl.Transaction> = Supplier { + SurfaceControl.Transaction() + }, +) : + DragToDesktopTransitionHandler( + context, + transitions, + taskDisplayAreaOrganizer, + interactionJankMonitor, + transactionSupplier + ) { + + /** + * @return layers in order: + * - appLayers - non-wallpaper, non-home tasks excluding the dragged task go at the bottom + * - homeLayers - home task on top of apps + * - wallpaperLayers - wallpaper on top of home + * - dragLayer - the dragged task on top of everything, there's only 1 dragged task + */ + override fun calculateStartDragToDesktopLayers(info: TransitionInfo): DragToDesktopLayers = + DragToDesktopLayers( + topAppLayer = info.changes.size, + topHomeLayer = info.changes.size * 2, + topWallpaperLayer = info.changes.size * 3, + dragLayer = info.changes.size * 3 + ) +} + +/** Desktop transition handler with spring based animation for the end drag to desktop transition */ +class SpringDragToDesktopTransitionHandler +@JvmOverloads +constructor( + context: Context, + transitions: Transitions, + taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + interactionJankMonitor: InteractionJankMonitor, + transactionSupplier: Supplier<SurfaceControl.Transaction> = Supplier { + SurfaceControl.Transaction() + }, +) : + DragToDesktopTransitionHandler( + context, + transitions, + taskDisplayAreaOrganizer, + interactionJankMonitor, + transactionSupplier + ) { + + private val positionSpringConfig = + PhysicsAnimator.SpringConfig(POSITION_SPRING_STIFFNESS, POSITION_SPRING_DAMPING_RATIO) + + private val sizeSpringConfig = + PhysicsAnimator.SpringConfig(SIZE_SPRING_STIFFNESS, SIZE_SPRING_DAMPING_RATIO) + + /** + * @return layers in order: + * - appLayers - below everything z < 0, effectively hides the leash + * - homeLayers - home task on top of apps, z in 0..<size + * - wallpaperLayers - wallpaper on top of home, z in size..<size*2 + * - dragLayer - the dragged task on top of everything, z == size*2 + */ + override fun calculateStartDragToDesktopLayers(info: TransitionInfo): DragToDesktopLayers = + DragToDesktopLayers( + topAppLayer = -1, + topHomeLayer = info.changes.size - 1, + topWallpaperLayer = info.changes.size * 2 - 1, + dragLayer = info.changes.size * 2 + ) + + override fun setupEndDragToDesktop( + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction + ) { + super.setupEndDragToDesktop(info, startTransaction, finishTransaction) + + val state = requireTransitionState() + val homeLeash = state.homeChange?.leash ?: error("Expects home leash to be non-null") + // Hide home on finish to prevent flickering when wallpaper activity flag is enabled + finishTransaction.hide(homeLeash) + // Setup freeform tasks before animation + state.freeformTaskChanges.forEach { change -> + val startScale = FREEFORM_TASKS_INITIAL_SCALE + val startX = + change.endAbsBounds.left + change.endAbsBounds.width() * (1 - startScale) / 2 + val startY = + change.endAbsBounds.top + change.endAbsBounds.height() * (1 - startScale) / 2 + startTransaction.setPosition(change.leash, startX, startY) + startTransaction.setScale(change.leash, startScale, startScale) + startTransaction.setAlpha(change.leash, 0f) + } + } + + override fun animateEndDragToDesktop( + startTransaction: SurfaceControl.Transaction, + startTransitionFinishCb: Transitions.TransitionFinishCallback + ) { + val state = requireTransitionState() + val draggedTaskChange = + state.draggedTaskChange ?: error("Expected non-null change of dragged task") + val draggedTaskLeash = draggedTaskChange.leash + val freeformTaskChanges = state.freeformTaskChanges + val startBounds = draggedTaskChange.startAbsBounds + val endBounds = draggedTaskChange.endAbsBounds + val currentVelocity = state.dragAnimator.computeCurrentVelocity() + + // Cancel any animation that may be currently playing; we will use the relevant + // details of that animation here. + state.dragAnimator.cancelAnimator() + // We still apply scale to task bounds; as we animate the bounds to their + // end value, animate scale to 1. + val startScale = state.dragAnimator.scale + val startPosition = state.dragAnimator.position + val startBoundsWithOffset = + Rect(startBounds).apply { offset(startPosition.x.toInt(), startPosition.y.toInt()) } + + dragToDesktopStateListener?.onCommitToDesktopAnimationStart(startTransaction) + // Accept the merge by applying the merging transaction (applied by #showResizeVeil) + // and finish callback. Show the veil and position the task at the first frame before + // starting the final animation. + onTaskResizeAnimationListener.onAnimationStart( + state.draggedTaskId, + startTransaction, + startBoundsWithOffset + ) + + val tx: SurfaceControl.Transaction = transactionSupplier.get() + PhysicsAnimator.getInstance(startBoundsWithOffset) + .spring( + FloatProperties.RECT_X, + endBounds.left.toFloat(), + currentVelocity.x, + positionSpringConfig + ) + .spring( + FloatProperties.RECT_Y, + endBounds.top.toFloat(), + currentVelocity.y, + positionSpringConfig + ) + .spring(FloatProperties.RECT_WIDTH, endBounds.width().toFloat(), sizeSpringConfig) + .spring(FloatProperties.RECT_HEIGHT, endBounds.height().toFloat(), sizeSpringConfig) + .addUpdateListener { animBounds, _ -> + val animFraction = + (animBounds.width() - startBounds.width()).toFloat() / + (endBounds.width() - startBounds.width()) + val animScale = startScale + animFraction * (1 - startScale) + // Freeform animation starts with freeform animation offset relative to the commit + // animation and plays until the commit animation ends. For instance: + // - if the freeform animation offset is `0.0` the freeform tasks animate alongside + // - if the freeform animation offset is `0.6` the freeform tasks will + // start animating at 60% fraction of the commit animation and will complete when + // the commit animation fraction is 100%. + // - if the freeform animation offset is `1.0` then freeform tasks will appear + // without animation after commit animation finishes. + val freeformAnimFraction = + if (FREEFORM_TASKS_ANIM_OFFSET != 1f) { + max(animFraction - FREEFORM_TASKS_ANIM_OFFSET, 0f) / + (1f - FREEFORM_TASKS_ANIM_OFFSET) + } else { + 0f + } + val freeformStartScale = FREEFORM_TASKS_INITIAL_SCALE + val freeformAnimScale = + freeformStartScale + freeformAnimFraction * (1 - freeformStartScale) + tx.apply { + // Update dragged task + setScale(draggedTaskLeash, animScale, animScale) + setPosition( + draggedTaskLeash, + animBounds.left.toFloat(), + animBounds.top.toFloat() + ) + // Update freeform tasks + freeformTaskChanges.forEach { + val startX = + it.endAbsBounds.left + + it.endAbsBounds.width() * (1 - freeformAnimScale) / 2 + val startY = + it.endAbsBounds.top + + it.endAbsBounds.height() * (1 - freeformAnimScale) / 2 + setPosition(it.leash, startX, startY) + setScale(it.leash, freeformAnimScale, freeformAnimScale) + setAlpha(it.leash, freeformAnimFraction) + } + } + onTaskResizeAnimationListener.onBoundsChange(state.draggedTaskId, tx, animBounds) + } + .withEndActions({ + onTaskResizeAnimationListener.onAnimationEnd(state.draggedTaskId) + startTransitionFinishCb.onTransitionFinished(/* wct = */ null) + clearState() + interactionJankMonitor.end(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE) + }) + .start() + } + + companion object { + /** The freeform tasks initial scale when committing the drag-to-desktop gesture. */ + private val FREEFORM_TASKS_INITIAL_SCALE = + propertyValue("freeform_tasks_initial_scale", scale = 100f, default = 0.9f) + + /** The freeform tasks animation offset relative to the whole animation duration. */ + private val FREEFORM_TASKS_ANIM_OFFSET = + propertyValue("freeform_tasks_anim_offset", scale = 100f, default = 0.5f) + + /** The spring force stiffness used to place the window into the final position. */ + private val POSITION_SPRING_STIFFNESS = + propertyValue("position_stiffness", default = SpringForce.STIFFNESS_LOW) + + /** The spring force damping ratio used to place the window into the final position. */ + private val POSITION_SPRING_DAMPING_RATIO = + propertyValue( + "position_damping_ratio", + scale = 100f, + default = SpringForce.DAMPING_RATIO_LOW_BOUNCY + ) + + /** The spring force stiffness used to resize the window into the final bounds. */ + private val SIZE_SPRING_STIFFNESS = + propertyValue("size_stiffness", default = SpringForce.STIFFNESS_LOW) + + /** The spring force damping ratio used to resize the window into the final bounds. */ + private val SIZE_SPRING_DAMPING_RATIO = + propertyValue( + "size_damping_ratio", + scale = 100f, + default = SpringForce.DAMPING_RATIO_NO_BOUNCY + ) + + /** Drag to desktop transition system properties group. */ + @VisibleForTesting + const val SYSTEM_PROPERTIES_GROUP = "persist.wm.debug.desktop_transitions.drag_to_desktop" + + /** + * Drag to desktop transition system property value with [name]. + * + * @param scale an optional scale to apply to the value read from the system property. + * @param default a default value to return if the system property isn't set. + */ + @VisibleForTesting + fun propertyValue(name: String, scale: Float = 1f, default: Float = 0f): Float = + SystemProperties.getInt( + /* key= */ "$SYSTEM_PROPERTIES_GROUP.$name", + /* def= */ (default * scale).toInt() + ) / scale } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java index e5b624f91c54..80e106f3990b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java @@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU; import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getEnterTransitionType; import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.isEnterDesktopModeTransition; @@ -39,7 +40,8 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; +import com.android.internal.jank.InteractionJankMonitor; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener; @@ -60,18 +62,21 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition public static final int FREEFORM_ANIMATION_DURATION = 336; private final List<IBinder> mPendingTransitionTokens = new ArrayList<>(); + private final InteractionJankMonitor mInteractionJankMonitor; private OnTaskResizeAnimationListener mOnTaskResizeAnimationListener; public EnterDesktopTaskTransitionHandler( - Transitions transitions) { - this(transitions, SurfaceControl.Transaction::new); + Transitions transitions, InteractionJankMonitor interactionJankMonitor) { + this(transitions, interactionJankMonitor, SurfaceControl.Transaction::new); } public EnterDesktopTaskTransitionHandler( Transitions transitions, + InteractionJankMonitor interactionJankMonitor, Supplier<SurfaceControl.Transaction> supplier) { mTransitions = transitions; + mInteractionJankMonitor = interactionJankMonitor; mTransactionSupplier = supplier; } @@ -175,6 +180,7 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition mOnTaskResizeAnimationListener.onAnimationEnd(taskInfo.taskId); mTransitions.getMainExecutor().execute( () -> finishCallback.onTransitionFinished(null)); + mInteractionJankMonitor.end(CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU); } }); animator.start(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java index 891f75cfdbda..dedd44f3950a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java @@ -29,6 +29,7 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; +import android.os.Handler; import android.os.IBinder; import android.util.DisplayMetrics; import android.view.SurfaceControl; @@ -42,7 +43,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; +import com.android.internal.jank.Cuj; +import com.android.internal.jank.InteractionJankMonitor; +import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.transition.Transitions; import java.util.ArrayList; @@ -60,6 +64,9 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH private final Context mContext; private final Transitions mTransitions; + private final InteractionJankMonitor mInteractionJankMonitor; + @ShellMainThread + private final Handler mHandler; private final List<IBinder> mPendingTransitionTokens = new ArrayList<>(); private Consumer<SurfaceControl.Transaction> mOnAnimationFinishedCallback; private final Supplier<SurfaceControl.Transaction> mTransactionSupplier; @@ -67,17 +74,25 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH public ExitDesktopTaskTransitionHandler( Transitions transitions, - Context context) { - this(transitions, SurfaceControl.Transaction::new, context); + Context context, + InteractionJankMonitor interactionJankMonitor, + @ShellMainThread Handler handler + ) { + this(transitions, SurfaceControl.Transaction::new, context, interactionJankMonitor, + handler); } private ExitDesktopTaskTransitionHandler( Transitions transitions, Supplier<SurfaceControl.Transaction> supplier, - Context context) { + Context context, + InteractionJankMonitor interactionJankMonitor, + @ShellMainThread Handler handler) { mTransitions = transitions; mTransactionSupplier = supplier; mContext = context; + mInteractionJankMonitor = interactionJankMonitor; + mHandler = handler; } /** @@ -146,6 +161,8 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH final int screenHeight = metrics.heightPixels; final SurfaceControl sc = change.getLeash(); final Rect endBounds = change.getEndAbsBounds(); + mInteractionJankMonitor + .begin(sc, mContext, mHandler, Cuj.CUJ_DESKTOP_MODE_EXIT_MODE); // Hide the first (fullscreen) frame because the animation will start from the freeform // size. startT.hide(sc) @@ -175,6 +192,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH if (mOnAnimationFinishedCallback != null) { mOnAnimationFinishedCallback.accept(finishT); } + mInteractionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_EXIT_MODE); mTransitions.getMainExecutor().execute( () -> finishCallback.onTransitionFinished(null)); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl index a7ec2037706d..b036e40e6e16 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl @@ -18,8 +18,8 @@ package com.android.wm.shell.desktopmode; import android.app.ActivityManager.RunningTaskInfo; import android.window.RemoteTransition; -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.desktopmode.IDesktopTaskListener; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; /** * Interface that is exposed to remote callers to manipulate desktop mode features. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl index 8ebdfdcf4731..c2acb87222d1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl @@ -27,4 +27,10 @@ interface IDesktopTaskListener { /** @deprecated this is no longer supported. */ oneway void onStashedChanged(int displayId, boolean stashed); + + /** + * Shows taskbar corner radius when running desktop tasks are updated if + * [hasTasksRequiringTaskbarRounding] is true. + */ + oneway void onTaskbarCornerRoundingUpdate(boolean hasTasksRequiringTaskbarRounding); }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt new file mode 100644 index 000000000000..f4df42cde10f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2024 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.desktopmode + +import android.animation.Animator +import android.animation.RectEvaluator +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Rect +import android.view.SurfaceControl +import android.widget.Toast +import androidx.core.animation.addListener +import com.android.internal.jank.Cuj +import com.android.internal.jank.InteractionJankMonitor +import com.android.wm.shell.R +import com.android.wm.shell.windowdecor.OnTaskRepositionAnimationListener +import java.util.function.Supplier + +/** Animates the task surface moving from its current drag position to its pre-drag position. */ +class ReturnToDragStartAnimator( + private val context: Context, + private val transactionSupplier: Supplier<SurfaceControl.Transaction>, + private val interactionJankMonitor: InteractionJankMonitor +) { + private var boundsAnimator: Animator? = null + private lateinit var taskRepositionAnimationListener: OnTaskRepositionAnimationListener + + constructor(context: Context, interactionJankMonitor: InteractionJankMonitor) : + this(context, Supplier { SurfaceControl.Transaction() }, interactionJankMonitor) + + /** Sets a listener for the start and end of the reposition animation. */ + fun setTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) { + taskRepositionAnimationListener = listener + } + + /** Builds new animator and starts animation of task leash reposition. */ + fun start( + taskId: Int, + taskSurface: SurfaceControl, + startBounds: Rect, + endBounds: Rect, + isResizable: Boolean + ) { + val tx = transactionSupplier.get() + + boundsAnimator?.cancel() + boundsAnimator = + ValueAnimator.ofObject(RectEvaluator(), startBounds, endBounds) + .setDuration(RETURN_TO_DRAG_START_ANIMATION_MS) + .apply { + addListener( + onStart = { + val startTransaction = transactionSupplier.get() + startTransaction + .setPosition( + taskSurface, + startBounds.left.toFloat(), + startBounds.top.toFloat() + ) + .show(taskSurface) + .apply() + taskRepositionAnimationListener.onAnimationStart(taskId) + }, + onEnd = { + val finishTransaction = transactionSupplier.get() + finishTransaction + .setPosition( + taskSurface, + endBounds.left.toFloat(), + endBounds.top.toFloat() + ) + .show(taskSurface) + .apply() + taskRepositionAnimationListener.onAnimationEnd(taskId) + boundsAnimator = null + if (!isResizable) { + Toast.makeText( + context, + R.string.desktop_mode_non_resizable_snap_text, + Toast.LENGTH_SHORT + ).show() + } + interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE) + } + ) + addUpdateListener { anim -> + val rect = anim.animatedValue as Rect + tx.setPosition(taskSurface, rect.left.toFloat(), rect.top.toFloat()) + .show(taskSurface) + .apply() + } + } + .also(ValueAnimator::start) + } + + companion object { + const val RETURN_TO_DRAG_START_ANIMATION_MS = 300L + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt index 88d0554669b7..96719fa2301a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt @@ -27,6 +27,8 @@ import android.window.TransitionInfo import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import androidx.core.animation.addListener +import com.android.internal.jank.Cuj +import com.android.internal.jank.InteractionJankMonitor import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener @@ -35,21 +37,32 @@ import java.util.function.Supplier /** Handles the animation of quick resizing of desktop tasks. */ class ToggleResizeDesktopTaskTransitionHandler( private val transitions: Transitions, - private val transactionSupplier: Supplier<SurfaceControl.Transaction> + private val transactionSupplier: Supplier<SurfaceControl.Transaction>, + private val interactionJankMonitor: InteractionJankMonitor ) : Transitions.TransitionHandler { private val rectEvaluator = RectEvaluator(Rect()) private lateinit var onTaskResizeAnimationListener: OnTaskResizeAnimationListener private var boundsAnimator: Animator? = null + private var initialBounds: Rect? = null constructor( - transitions: Transitions - ) : this(transitions, Supplier { SurfaceControl.Transaction() }) + transitions: Transitions, + interactionJankMonitor: InteractionJankMonitor + ) : this(transitions, Supplier { SurfaceControl.Transaction() }, interactionJankMonitor) - /** Starts a quick resize transition. */ - fun startTransition(wct: WindowContainerTransaction) { + /** + * Starts a quick resize transition. + * + * @param wct WindowContainerTransaction that will update core about the task changes applied + * @param taskLeashBounds current bounds of the task leash (Note: not guaranteed to be the + * bounds of the actual task). This is provided so that the animation + * resizing can begin where the task leash currently is for smoother UX. + */ + fun startTransition(wct: WindowContainerTransaction, taskLeashBounds: Rect? = null) { transitions.startTransition(TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE, wct, this) + initialBounds = taskLeashBounds } fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) { @@ -66,7 +79,7 @@ class ToggleResizeDesktopTaskTransitionHandler( val change = findRelevantChange(info) val leash = change.leash val taskId = checkNotNull(change.taskInfo).taskId - val startBounds = change.startAbsBounds + val startBounds = initialBounds ?: change.startAbsBounds val endBounds = change.endAbsBounds val tx = transactionSupplier.get() @@ -88,7 +101,7 @@ class ToggleResizeDesktopTaskTransitionHandler( onTaskResizeAnimationListener.onAnimationStart( taskId, startTransaction, - startBounds + startBounds, ) }, onEnd = { @@ -102,7 +115,10 @@ class ToggleResizeDesktopTaskTransitionHandler( .show(leash) onTaskResizeAnimationListener.onAnimationEnd(taskId) finishCallback.onTransitionFinished(null) + initialBounds = null boundsAnimator = null + interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW) + interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE) } ) addUpdateListener { anim -> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt new file mode 100644 index 000000000000..7ae537088832 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 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.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.graphics.Rect +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** Repository to observe caption state. */ +class WindowDecorCaptionHandleRepository { + private val _captionStateFlow = MutableStateFlow<CaptionState>(CaptionState.NoCaption) + /** Observer for app handle state changes. */ + val captionStateFlow: StateFlow<CaptionState> = _captionStateFlow + + /** Notifies [captionStateFlow] if there is a change to caption state. */ + fun notifyCaptionChanged(captionState: CaptionState) { + _captionStateFlow.value = captionState + } +} + +/** + * Represents the current status of the caption. + * + * It can be one of three options: + * * [AppHandle]: Indicating that there is at least one visible app handle on the screen. + * * [AppHeader]: Indicating that there is at least one visible app chip on the screen. + * * [NoCaption]: Signifying that no caption handle is currently visible on the device. + */ +sealed class CaptionState { + data class AppHandle( + val runningTaskInfo: RunningTaskInfo, + val isHandleMenuExpanded: Boolean, + val globalAppHandleBounds: Rect + ) : CaptionState() + + data class AppHeader( + val runningTaskInfo: RunningTaskInfo, + val isHeaderMenuExpanded: Boolean, + val globalAppChipBounds: Rect + ) : CaptionState() + + data object NoCaption : CaptionState() +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt new file mode 100644 index 000000000000..6013e97977d8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2024 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.desktopmode.education + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.os.SystemProperties +import com.android.window.flags.Flags +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository +import com.android.wm.shell.shared.annotations.ShellMainThread +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +/** + * Controls app handle education end to end. + * + * Listen to the user trigger for app handle education, calls an api to check if the education + * should be shown and calls an api to show education. + */ +@OptIn(kotlinx.coroutines.FlowPreview::class) +@kotlinx.coroutines.ExperimentalCoroutinesApi +class AppHandleEducationController( + private val appHandleEducationFilter: AppHandleEducationFilter, + shellTaskOrganizer: ShellTaskOrganizer, + private val appHandleEducationDatastoreRepository: AppHandleEducationDatastoreRepository, + @ShellMainThread private val applicationCoroutineScope: CoroutineScope +) { + init { + runIfEducationFeatureEnabled { + // TODO: b/361038716 - Use app handle state flow instead of focus task change flow + val focusTaskChangeFlow = focusTaskChangeFlow(shellTaskOrganizer) + applicationCoroutineScope.launch { + // Central block handling the app's educational flow end-to-end. + // This flow listens to the changes to the result of + // [WindowingEducationProto#hasEducationViewedTimestampMillis()] in datastore proto object + isEducationViewedFlow() + .flatMapLatest { isEducationViewed -> + if (isEducationViewed) { + // If the education is viewed then return emptyFlow() that completes immediately. + // This will help us to not listen to focus task changes after the education has + // been viewed already. + emptyFlow() + } else { + // This flow listens for focus task changes, which trigger the app handle education. + focusTaskChangeFlow + .filter { runningTaskInfo -> + runningTaskInfo.topActivityInfo?.packageName?.let { + appHandleEducationFilter.shouldShowAppHandleEducation(it) + } ?: false && runningTaskInfo.windowingMode != WINDOWING_MODE_FREEFORM + } + .distinctUntilChanged() + } + } + .debounce( + APP_HANDLE_EDUCATION_DELAY) // Wait for few seconds, if the focus task changes. + // During the delay then current emission will be cancelled. + .flowOn(Dispatchers.IO) + .collectLatest { + // Fire and forget show education suspend function, manage entire lifecycle of + // tooltip in UI class. + } + } + } + } + + private inline fun runIfEducationFeatureEnabled(block: () -> Unit) { + if (Flags.enableDesktopWindowingAppHandleEducation()) block() + } + + private fun isEducationViewedFlow(): Flow<Boolean> = + appHandleEducationDatastoreRepository.dataStoreFlow + .map { preferences -> preferences.hasEducationViewedTimestampMillis() } + .distinctUntilChanged() + + private fun focusTaskChangeFlow(shellTaskOrganizer: ShellTaskOrganizer): Flow<RunningTaskInfo> = + callbackFlow { + val focusTaskChange = ShellTaskOrganizer.FocusListener { taskInfo -> trySend(taskInfo) } + shellTaskOrganizer.addFocusListener(focusTaskChange) + awaitClose { shellTaskOrganizer.removeFocusListener(focusTaskChange) } + } + + private companion object { + val APP_HANDLE_EDUCATION_DELAY: Long + get() = SystemProperties.getLong("persist.windowing_app_handle_education_delay", 3000L) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt new file mode 100644 index 000000000000..51bdb40e12e6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2024 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.desktopmode.education + +import android.annotation.IntegerRes +import android.app.usage.UsageStatsManager +import android.content.Context +import android.os.SystemClock +import android.provider.Settings.Secure +import com.android.wm.shell.R +import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository +import com.android.wm.shell.desktopmode.education.data.WindowingEducationProto +import java.time.Duration + +/** Filters incoming app handle education triggers based on set conditions. */ +class AppHandleEducationFilter( + private val context: Context, + private val appHandleEducationDatastoreRepository: AppHandleEducationDatastoreRepository +) { + private val usageStatsManager = + context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager + + /** Returns true if conditions to show app handle education are met, returns false otherwise. */ + suspend fun shouldShowAppHandleEducation(focusAppPackageName: String): Boolean { + val windowingEducationProto = appHandleEducationDatastoreRepository.windowingEducationProto() + return isFocusAppInAllowlist(focusAppPackageName) && + !isOtherEducationShowing() && + hasSufficientTimeSinceSetup() && + !isEducationViewedBefore(windowingEducationProto) && + !isFeatureUsedBefore(windowingEducationProto) && + hasMinAppUsage(windowingEducationProto, focusAppPackageName) + } + + private fun isFocusAppInAllowlist(focusAppPackageName: String): Boolean = + focusAppPackageName in + context.resources.getStringArray( + R.array.desktop_windowing_app_handle_education_allowlist_apps) + + // TODO: b/350953004 - Add checks based on App compat + // TODO: b/350951797 - Add checks based on PKT tips education + private fun isOtherEducationShowing(): Boolean = isTaskbarEducationShowing() + + private fun isTaskbarEducationShowing(): Boolean = + Secure.getInt(context.contentResolver, Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0) == 1 + + private fun hasSufficientTimeSinceSetup(): Boolean = + Duration.ofMillis(SystemClock.elapsedRealtime()) > + convertIntegerResourceToDuration( + R.integer.desktop_windowing_education_required_time_since_setup_seconds) + + private fun isEducationViewedBefore(windowingEducationProto: WindowingEducationProto): Boolean = + windowingEducationProto.hasEducationViewedTimestampMillis() + + private fun isFeatureUsedBefore(windowingEducationProto: WindowingEducationProto): Boolean = + windowingEducationProto.hasFeatureUsedTimestampMillis() + + private suspend fun hasMinAppUsage( + windowingEducationProto: WindowingEducationProto, + focusAppPackageName: String + ): Boolean = + (launchCountByPackageName(windowingEducationProto)[focusAppPackageName] ?: 0) >= + context.resources.getInteger(R.integer.desktop_windowing_education_min_app_launch_count) + + private suspend fun launchCountByPackageName( + windowingEducationProto: WindowingEducationProto + ): Map<String, Int> = + if (isAppUsageCacheStale(windowingEducationProto)) { + // Query and return user stats, update cache in datastore + getAndCacheAppUsageStats() + } else { + // Return cached usage stats + windowingEducationProto.appHandleEducation.appUsageStatsMap + } + + private fun isAppUsageCacheStale(windowingEducationProto: WindowingEducationProto): Boolean { + val currentTime = currentTimeInDuration() + val lastUpdateTime = + Duration.ofMillis( + windowingEducationProto.appHandleEducation.appUsageStatsLastUpdateTimestampMillis) + val appUsageStatsCachingInterval = + convertIntegerResourceToDuration( + R.integer.desktop_windowing_education_app_usage_cache_interval_seconds) + return (currentTime - lastUpdateTime) > appUsageStatsCachingInterval + } + + private suspend fun getAndCacheAppUsageStats(): Map<String, Int> { + val currentTime = currentTimeInDuration() + val appUsageStats = queryAppUsageStats() + appHandleEducationDatastoreRepository.updateAppUsageStats(appUsageStats, currentTime) + return appUsageStats + } + + private fun queryAppUsageStats(): Map<String, Int> { + val endTime = currentTimeInDuration() + val appLaunchInterval = + convertIntegerResourceToDuration( + R.integer.desktop_windowing_education_app_launch_interval_seconds) + val startTime = endTime - appLaunchInterval + + return usageStatsManager + .queryAndAggregateUsageStats(startTime.toMillis(), endTime.toMillis()) + .mapValues { it.value.appLaunchCount } + } + + private fun convertIntegerResourceToDuration(@IntegerRes resourceId: Int): Duration = + Duration.ofSeconds(context.resources.getInteger(resourceId).toLong()) + + private fun currentTimeInDuration(): Duration = Duration.ofMillis(System.currentTimeMillis()) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt new file mode 100644 index 000000000000..f420c5be456f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2024 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.desktopmode.education.data + +import android.content.Context +import android.util.Log +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.core.Serializer +import androidx.datastore.dataStoreFile +import com.android.framework.protobuf.InvalidProtocolBufferException +import com.android.internal.annotations.VisibleForTesting +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.time.Duration +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first + +/** + * Manages interactions with the App Handle education datastore. + * + * This class provides a layer of abstraction between the UI/business logic and the underlying + * DataStore. + */ +class AppHandleEducationDatastoreRepository +@VisibleForTesting +constructor(private val dataStore: DataStore<WindowingEducationProto>) { + constructor( + context: Context + ) : this( + DataStoreFactory.create( + serializer = WindowingEducationProtoSerializer, + produceFile = { context.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_FILEPATH) })) + + /** Provides dataStore.data flow and handles exceptions thrown during collection */ + val dataStoreFlow: Flow<WindowingEducationProto> = + dataStore.data.catch { exception -> + // dataStore.data throws an IOException when an error is encountered when reading data + if (exception is IOException) { + Log.e( + TAG, + "Error in reading app handle education related data from datastore, data is " + + "stored in a file named $APP_HANDLE_EDUCATION_DATASTORE_FILEPATH", + exception) + } else { + throw exception + } + } + + /** + * Reads and returns the [WindowingEducationProto] Proto object from the DataStore. If the + * DataStore is empty or there's an error reading, it returns the default value of Proto. + */ + suspend fun windowingEducationProto(): WindowingEducationProto = dataStoreFlow.first() + + /** + * Updates [AppHandleEducation.appUsageStats] and + * [AppHandleEducation.appUsageStatsLastUpdateTimestampMillis] fields in datastore with + * [appUsageStats] and [appUsageStatsLastUpdateTimestamp]. + */ + suspend fun updateAppUsageStats( + appUsageStats: Map<String, Int>, + appUsageStatsLastUpdateTimestamp: Duration + ) { + val currentAppHandleProto = windowingEducationProto().appHandleEducation.toBuilder() + currentAppHandleProto + .putAllAppUsageStats(appUsageStats) + .setAppUsageStatsLastUpdateTimestampMillis(appUsageStatsLastUpdateTimestamp.toMillis()) + dataStore.updateData { preferences: WindowingEducationProto -> + preferences.toBuilder().setAppHandleEducation(currentAppHandleProto).build() + } + } + + companion object { + private const val TAG = "AppHandleEducationDatastoreRepository" + private const val APP_HANDLE_EDUCATION_DATASTORE_FILEPATH = "app_handle_education.pb" + + object WindowingEducationProtoSerializer : Serializer<WindowingEducationProto> { + + override val defaultValue: WindowingEducationProto = + WindowingEducationProto.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): WindowingEducationProto = + try { + WindowingEducationProto.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + + override suspend fun writeTo(windowingProto: WindowingEducationProto, output: OutputStream) = + windowingProto.writeTo(output) + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto new file mode 100644 index 000000000000..d29ec53d9c61 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 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. + */ + +syntax = "proto2"; + +option java_package = "com.android.wm.shell.desktopmode.education.data"; +option java_multiple_files = true; + +// Desktop Windowing education data +message WindowingEducationProto { + // Timestamp in milliseconds of when the education was last viewed. + optional int64 education_viewed_timestamp_millis = 1; + // Timestamp in milliseconds of when the feature was last used. + optional int64 feature_used_timestamp_millis = 2; + oneof education_data { + // Fields specific to app handle education + AppHandleEducation app_handle_education = 3; + } + + message AppHandleEducation { + // Map that stores app launch count for corresponding package + map<string, int32> app_usage_stats = 1; + // Timestamp of when app_usage_stats was last cached + optional int64 app_usage_stats_last_update_timestamp_millis = 2; + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt new file mode 100644 index 000000000000..3f41d7cf4e86 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2024 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.desktopmode.persistence + +import android.content.Context +import android.util.ArraySet +import android.util.Log +import android.view.Display.DEFAULT_DISPLAY +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.core.Serializer +import androidx.datastore.dataStoreFile +import com.android.framework.protobuf.InvalidProtocolBufferException +import com.android.wm.shell.shared.annotations.ShellBackgroundThread +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first + +/** + * Persistent repository for storing desktop mode related data. + * + * The main constructor is public only for testing purposes. + */ +class DesktopPersistentRepository( + private val dataStore: DataStore<DesktopPersistentRepositories>, +) { + constructor( + context: Context, + @ShellBackgroundThread bgCoroutineScope: CoroutineScope, + ) : this( + DataStoreFactory.create( + serializer = DesktopPersistentRepositoriesSerializer, + produceFile = { context.dataStoreFile(DESKTOP_REPOSITORIES_DATASTORE_FILE) }, + scope = bgCoroutineScope)) + + /** Provides `dataStore.data` flow and handles exceptions thrown during collection */ + private val dataStoreFlow: Flow<DesktopPersistentRepositories> = + dataStore.data.catch { exception -> + // dataStore.data throws an IOException when an error is encountered when reading data + if (exception is IOException) { + Log.e( + TAG, + "Error in reading desktop mode related data from datastore, data is " + + "stored in a file named $DESKTOP_REPOSITORIES_DATASTORE_FILE", + exception) + } else { + throw exception + } + } + + /** + * Reads and returns the [DesktopRepositoryState] proto object from the DataStore for a user. If + * the DataStore is empty or there's an error reading, it returns the default value of Proto. + */ + private suspend fun getDesktopRepositoryState( + userId: Int = DEFAULT_USER_ID + ): DesktopRepositoryState = + try { + dataStoreFlow + .first() + .desktopRepoByUserMap + .getOrDefault(userId, DesktopRepositoryState.getDefaultInstance()) + } catch (e: Exception) { + Log.e(TAG, "Unable to read from datastore", e) + DesktopRepositoryState.getDefaultInstance() + } + + /** + * Reads the [Desktop] of a desktop filtering by the [userId] and [desktopId]. Executes the + * [callback] using the [mainCoroutineScope]. + */ + suspend fun readDesktop( + userId: Int = DEFAULT_USER_ID, + desktopId: Int = DEFAULT_DESKTOP_ID, + ): Desktop = + try { + val repository = getDesktopRepositoryState(userId) + repository.getDesktopOrThrow(desktopId) + } catch (e: Exception) { + Log.e(TAG, "Unable to get desktop info from persistent repository", e) + Desktop.getDefaultInstance() + } + + /** Adds or updates a desktop stored in the datastore */ + suspend fun addOrUpdateDesktop( + userId: Int = DEFAULT_USER_ID, + desktopId: Int = 0, + visibleTasks: ArraySet<Int> = ArraySet(), + minimizedTasks: ArraySet<Int> = ArraySet(), + freeformTasksInZOrder: ArrayList<Int> = ArrayList(), + ) { + // TODO: b/367609270 - Improve the API to support multi-user + try { + dataStore.updateData { desktopPersistentRepositories: DesktopPersistentRepositories -> + val currentRepository = + desktopPersistentRepositories.getDesktopRepoByUserOrDefault( + userId, DesktopRepositoryState.getDefaultInstance()) + val desktop = + getDesktop(currentRepository, desktopId) + .toBuilder() + .updateTaskStates(visibleTasks, minimizedTasks) + .updateZOrder(freeformTasksInZOrder) + + desktopPersistentRepositories + .toBuilder() + .putDesktopRepoByUser( + userId, + currentRepository + .toBuilder() + .putDesktop(desktopId, desktop.build()) + .build()) + .build() + } + } catch (exception: IOException) { + Log.e( + TAG, + "Error in updating desktop mode related data, data is " + + "stored in a file named $DESKTOP_REPOSITORIES_DATASTORE_FILE", + exception) + } + } + + private fun getDesktop(currentRepository: DesktopRepositoryState, desktopId: Int): Desktop = + // If there are no desktops set up, create one on the default display + currentRepository.getDesktopOrDefault( + desktopId, + Desktop.newBuilder().setDesktopId(desktopId).setDisplayId(DEFAULT_DISPLAY).build()) + + companion object { + private const val TAG = "DesktopPersistenceRepo" + private const val DESKTOP_REPOSITORIES_DATASTORE_FILE = "desktop_persistent_repositories.pb" + + private const val DEFAULT_USER_ID = 1000 + private const val DEFAULT_DESKTOP_ID = 0 + + object DesktopPersistentRepositoriesSerializer : Serializer<DesktopPersistentRepositories> { + + override val defaultValue: DesktopPersistentRepositories = + DesktopPersistentRepositories.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): DesktopPersistentRepositories = + try { + DesktopPersistentRepositories.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + + override suspend fun writeTo(t: DesktopPersistentRepositories, output: OutputStream) = + t.writeTo(output) + } + + private fun Desktop.Builder.updateTaskStates( + visibleTasks: ArraySet<Int>, + minimizedTasks: ArraySet<Int> + ): Desktop.Builder { + clearTasksByTaskId() + putAllTasksByTaskId( + visibleTasks.associateWith { + createDesktopTask(it, state = DesktopTaskState.VISIBLE) + }) + putAllTasksByTaskId( + minimizedTasks.associateWith { + createDesktopTask(it, state = DesktopTaskState.MINIMIZED) + }) + return this + } + + private fun Desktop.Builder.updateZOrder( + freeformTasksInZOrder: ArrayList<Int> + ): Desktop.Builder { + clearZOrderedTasks() + addAllZOrderedTasks(freeformTasksInZOrder) + return this + } + + private fun createDesktopTask( + taskId: Int, + state: DesktopTaskState = DesktopTaskState.VISIBLE + ): DesktopTask = + DesktopTask.newBuilder().setTaskId(taskId).setDesktopTaskState(state).build() + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/persistent_desktop_repositories.proto b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/persistent_desktop_repositories.proto new file mode 100644 index 000000000000..010523162cec --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/persistent_desktop_repositories.proto @@ -0,0 +1,33 @@ +syntax = "proto2"; + +option java_package = "com.android.wm.shell.desktopmode.persistence"; +option java_multiple_files = true; + +// Represents the state of a task in desktop. +enum DesktopTaskState { + VISIBLE = 0; + MINIMIZED = 1; +} + +message DesktopTask { + optional int32 task_id = 1; + optional DesktopTaskState desktop_task_state= 2; +} + +message Desktop { + optional int32 display_id = 1; + optional int32 desktop_id = 2; + // Stores a mapping between task id and the tasks. The key is the task id. + map<int32, DesktopTask> tasks_by_task_id = 3; + repeated int32 z_ordered_tasks = 4; +} + +message DesktopRepositoryState { + // Stores a mapping between a repository and the desktops in it. The key is the desktop id. + map<int32, Desktop> desktop = 1; +} + +message DesktopPersistentRepositories { + // Stores a mapping between a user and their desktop repository. The key is the user id. + map<int32, DesktopRepositoryState> desktop_repo_by_user = 1; +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md index 0acc7df98d1c..faa97ac4512f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md @@ -98,9 +98,8 @@ Don't: ### Exposing shared code for use in Launcher Launcher doesn't currently build against the Shell library, but needs to have access to some shared AIDL interfaces and constants. Currently, all AIDL files, and classes under the -`com.android.wm.shell.util` package are automatically built into the `SystemUISharedLib` that +`com.android.wm.shell.shared` package are automatically built into the `SystemUISharedLib` that Launcher uses. -If the new code doesn't fall into those categories, they can be added explicitly in the Shell's -[Android.bp](/libs/WindowManager/Shell/Android.bp) file under the -`wm_shell_util-sources` filegroup.
\ No newline at end of file +If the new code doesn't fall into those categories, they should be moved to the Shell shared +package (`com.android.wm.shell.shared`) under the `WindowManager-Shell-shared` library.
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md index 438aa768165e..72d1a76b17e3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md @@ -27,10 +27,13 @@ building to check the log state (is enabled) before printing the print format st traces in Winscope) ### Kotlin +Kotlin protologging is supported but not as optimized as in Java. -Protolog tool does not yet have support for Kotlin code (see [b/168581922](https://b.corp.google.com/issues/168581922)). -For logging in Kotlin, use the [KtProtoLog](/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt) -class which has a similar API to the Java ProtoLog class. +The Protolog tool does not yet have support for Kotlin code ([b/168581922](https://b.corp.google.com/issues/168581922)). + +What this implies is that ProtoLogs are not pre-processed to extract the static strings out when used in Kotlin. So, +there is no memory gain when using ProtoLogging in Kotlin. The logs will still be traced to Perfetto, but with a subtly +worse performance due to the additional string interning that needs to be done at run time instead of at build time. ### Enabling ProtoLog command line logging Run these commands to enable protologs (in logcat) for WM Core ([list of all core tags](/core/java/com/android/internal/protolog/ProtoLogGroup.java)): @@ -68,12 +71,12 @@ adb shell dumpsys SurfaceFlinger ## Tracing global SurfaceControl transaction updates While Winscope traces are very useful, it sometimes doesn't give you enough information about which -part of the code is initiating the transaction updates. In such cases, it can be helpful to get +part of the code is initiating the transaction updates. In such cases, it can be helpful to get stack traces when specific surface transaction calls are made, which is possible by enabling the following system properties for example: ```shell # Enabling -adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha # matches the name of the SurfaceControlTransaction method +adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha,setPosition # matches the name of the SurfaceControlTransaction methods adb shell setprop persist.wm.debug.sc.tx.log_match_name com.android.systemui # matches the name of the surface adb reboot adb logcat -s "SurfaceControlRegistry" @@ -81,13 +84,25 @@ adb logcat -s "SurfaceControlRegistry" # Disabling logging adb shell setprop persist.wm.debug.sc.tx.log_match_call \"\" adb shell setprop persist.wm.debug.sc.tx.log_match_name \"\" -adb reboot ``` +A reboot is required to enable the logging. Once enabled, reboot is not needed to update the +properties. + It is not necessary to set both `log_match_call` and `log_match_name`, but note logs can be quite noisy if unfiltered. -## Tracing activity starts in the app process +It can sometimes be useful to trace specific logs and when they are applied (sometimes we build +transactions that can be applied later). You can do this by adding the "merge" and "apply" calls to +the set of requested calls: +```shell +# Enabling +adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha,merge,apply # apply will dump logs of each setAlpha or merge call on that tx +adb reboot +adb logcat -s "SurfaceControlRegistry" +``` + +## Tracing activity starts & finishes in the app process It's sometimes useful to know when to see a stack trace of when an activity starts in the app code (ie. if you are repro'ing a bug related to activity starts). You can enable this system property to @@ -103,6 +118,19 @@ adb shell setprop persist.wm.debug.start_activity \"\" adb reboot ``` +Likewise, to trace where a finish() call may be made in the app process, you can enable this system +property: +```shell +# Enabling +adb shell setprop persist.wm.debug.finish_activity true +adb reboot +adb logcat -s "Instrumentation" + +# Disabling +adb shell setprop persist.wm.debug.finish_activity \"\" +adb reboot +``` + ## Dumps Because the Shell library is built as a part of SystemUI, dumping the state is currently done as a 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 c374eb8e8f03..22e8dc186e9b 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 @@ -26,13 +26,12 @@ import static android.view.DragEvent.ACTION_DROP; import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; -import static android.view.WindowManager.LayoutParams.MATCH_PARENT; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; -import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DRAG_AND_DROP; +import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_DRAG_AND_DROP; import android.app.ActivityManager; import android.app.ActivityTaskManager; @@ -52,6 +51,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.FrameLayout; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; @@ -59,15 +59,17 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.logging.UiEventLogger; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.annotations.ExternalMainThread; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; @@ -85,6 +87,7 @@ import java.util.function.Function; public class DragAndDropController implements RemoteCallable<DragAndDropController>, GlobalDragListener.GlobalDragListenerCallback, DisplayController.OnDisplaysChangedListener, + ShellTaskOrganizer.TaskVanishedListener, View.OnDragListener, ComponentCallbacks2 { private static final String TAG = DragAndDropController.class.getSimpleName(); @@ -92,6 +95,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll private final Context mContext; private final ShellController mShellController; private final ShellCommandHandler mShellCommandHandler; + private final ShellTaskOrganizer mShellTaskOrganizer; private final DisplayController mDisplayController; private final DragAndDropEventLogger mLogger; private final IconProvider mIconProvider; @@ -123,7 +127,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll * drag. */ default boolean onUnhandledDrag(@NonNull PendingIntent launchIntent, - @NonNull SurfaceControl dragSurface, + @NonNull DragEvent dragEvent, @NonNull Consumer<Boolean> onFinishCallback) { return false; } @@ -133,6 +137,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll ShellInit shellInit, ShellController shellController, ShellCommandHandler shellCommandHandler, + ShellTaskOrganizer shellTaskOrganizer, DisplayController displayController, UiEventLogger uiEventLogger, IconProvider iconProvider, @@ -142,6 +147,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll mContext = context; mShellController = shellController; mShellCommandHandler = shellCommandHandler; + mShellTaskOrganizer = shellTaskOrganizer; mDisplayController = displayController; mLogger = new DragAndDropEventLogger(uiEventLogger); mIconProvider = iconProvider; @@ -163,6 +169,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll }, 0); mShellController.addExternalInterface(KEY_EXTRA_SHELL_DRAG_AND_DROP, this::createExternalInterface, this); + mShellTaskOrganizer.addTaskVanishedListener(this); mShellCommandHandler.addDumpCallback(this::dump, this); mGlobalDragListener.setListener(this); } @@ -239,9 +246,8 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll R.layout.global_drop_target, null); rootView.setOnDragListener(this); rootView.setVisibility(View.INVISIBLE); - DragLayout dragLayout = new DragLayout(context, mSplitScreen, mIconProvider); - rootView.addView(dragLayout, - new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + DragLayoutProvider dragLayout = new DragLayout(context, mSplitScreen, mIconProvider); + dragLayout.addDraggingView(rootView); try { wm.addView(rootView, layoutParams); addDisplayDropTarget(displayId, context, wm, rootView, dragLayout); @@ -253,7 +259,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll @VisibleForTesting void addDisplayDropTarget(int displayId, Context context, WindowManager wm, - FrameLayout rootView, DragLayout dragLayout) { + FrameLayout rootView, DragLayoutProvider dragLayout) { mDisplayDropTargets.put(displayId, new PerDisplay(displayId, context, wm, rootView, dragLayout)); } @@ -281,6 +287,34 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll } @Override + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + if (taskInfo.baseIntent == null) { + // Invalid info + return; + } + // Find the active drag + PerDisplay pd = null; + for (int i = 0; i < mDisplayDropTargets.size(); i++) { + final PerDisplay iPd = mDisplayDropTargets.valueAt(i); + if (iPd.isHandlingDrag) { + pd = iPd; + break; + } + } + if (pd == null || pd.activeDragCount <= 0 || !pd.isHandlingDrag) { + // Not currently dragging + return; + } + + // Update the drag session + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Handling vanished task: id=%d component=%s", taskInfo.taskId, + taskInfo.baseIntent.getComponent()); + pd.dragSession.updateRunningTask(); + pd.dragLayout.updateSession(pd.dragSession); + } + + @Override public boolean onDrag(View target, DragEvent event) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Drag event: action=%s x=%f y=%f xOffset=%f yOffset=%f", @@ -294,13 +328,23 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll return false; } + DragSession dragSession = null; if (event.getAction() == ACTION_DRAG_STARTED) { mActiveDragDisplay = displayId; - pd.isHandlingDrag = DragUtils.canHandleDrag(event); + dragSession = new DragSession(ActivityTaskManager.getInstance(), + mDisplayController.getDisplayLayout(displayId), event.getClipData(), + event.getDragFlags()); + dragSession.initialize(); + final ActivityManager.RunningTaskInfo taskInfo = dragSession.runningTaskInfo; + // Desktop tasks will have their own drag handling. + final boolean isDesktopDrag = taskInfo != null && taskInfo.isFreeform() + && DesktopModeStatus.canEnterDesktopMode(mContext); + pd.isHandlingDrag = DragUtils.canHandleDrag(event) && !isDesktopDrag; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, - "Clip description: handlingDrag=%b itemCount=%d mimeTypes=%s", + "Clip description: handlingDrag=%b itemCount=%d mimeTypes=%s flags=%s", pd.isHandlingDrag, event.getClipData().getItemCount(), - DragUtils.getMimeTypesConcatenated(description)); + DragUtils.getMimeTypesConcatenated(description), + DragUtils.dragFlagsToString(event.getDragFlags())); } if (!pd.isHandlingDrag) { @@ -313,13 +357,15 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll Slog.w(TAG, "Unexpected drag start during an active drag"); return false; } - // TODO(b/290391688): Also update the session data with task stack changes - pd.dragSession = new DragSession(ActivityTaskManager.getInstance(), - mDisplayController.getDisplayLayout(displayId), event.getClipData(), - event.getDragFlags()); - pd.dragSession.update(); + pd.dragSession = dragSession; pd.activeDragCount++; pd.dragLayout.prepare(pd.dragSession, mLogger.logStart(pd.dragSession)); + if (pd.dragSession.hideDragSourceTaskId != -1) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Hiding task surface: taskId=%d", pd.dragSession.hideDragSourceTaskId); + mShellTaskOrganizer.setTaskSurfaceVisibility( + pd.dragSession.hideDragSourceTaskId, false /* visible */); + } setDropTargetWindowVisibility(pd, View.VISIBLE); notifyListeners(l -> { l.onDragStarted(); @@ -349,6 +395,13 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll if (pd.dragLayout.hasDropped()) { mLogger.logDrop(); } else { + if (pd.dragSession.hideDragSourceTaskId != -1) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Re-showing task surface: taskId=%d", + pd.dragSession.hideDragSourceTaskId); + mShellTaskOrganizer.setTaskSurfaceVisibility( + pd.dragSession.hideDragSourceTaskId, true /* visible */); + } pd.activeDragCount--; pd.dragLayout.hide(event, () -> { if (pd.activeDragCount == 0) { @@ -389,7 +442,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll } final boolean handled = notifyListeners( - l -> l.onUnhandledDrag(launchIntent, dragEvent.getDragSurface(), onFinishCallback)); + l -> l.onUnhandledDrag(launchIntent, dragEvent, onFinishCallback)); if (!handled) { // Nobody handled this, we still have to notify WM onFinishCallback.accept(false); @@ -402,7 +455,16 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll private boolean handleDrop(DragEvent event, PerDisplay pd) { final SurfaceControl dragSurface = event.getDragSurface(); pd.activeDragCount--; - return pd.dragLayout.drop(event, dragSurface, () -> { + // Find the token of the task to hide as a part of entering split + WindowContainerToken hideTaskToken = null; + if (pd.dragSession.hideDragSourceTaskId != -1) { + ActivityManager.RunningTaskInfo info = mShellTaskOrganizer.getRunningTaskInfo( + pd.dragSession.hideDragSourceTaskId); + if (info != null) { + hideTaskToken = info.token; + } + } + return pd.dragLayout.drop(event, dragSurface, hideTaskToken, () -> { if (pd.activeDragCount == 0) { // Hide the window if another drag hasn't been started while animating the drop setDropTargetWindowVisibility(pd, View.INVISIBLE); @@ -500,7 +562,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll final Context context; final WindowManager wm; final FrameLayout rootView; - final DragLayout dragLayout; + final DragLayoutProvider dragLayout; // Tracks whether the window has fully drawn since it was last made visible boolean hasDrawn; @@ -511,7 +573,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll // The active drag session DragSession dragSession; - PerDisplay(int dispId, Context c, WindowManager w, FrameLayout rv, DragLayout dl) { + PerDisplay(int dispId, Context c, WindowManager w, FrameLayout rv, DragLayoutProvider dl) { displayId = dispId; context = c; wm = w; 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 4bb10dfdf8c6..dfa24370590a 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 @@ -20,17 +20,15 @@ import static android.app.StatusBarManager.DISABLE_NONE; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.content.pm.ActivityInfo.CONFIG_ASSETS_PATHS; import static android.content.pm.ActivityInfo.CONFIG_UI_MODE; -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitScreenUtils.getResizingBackgroundColor; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_BOTTOM; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_LEFT; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_RIGHT; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_TOP; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -41,36 +39,41 @@ import android.app.StatusBarManager; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Color; import android.graphics.Insets; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.Drawable; import android.view.DragEvent; import android.view.SurfaceControl; +import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.view.WindowInsets.Type; import android.widget.LinearLayout; +import android.window.WindowContainerToken; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.internal.logging.InstanceId; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; -import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.split.SplitScreenUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.animation.Interpolators; import com.android.wm.shell.splitscreen.SplitScreenController; import java.io.PrintWriter; -import java.util.ArrayList; +import java.util.List; /** * Coordinates the visible drop targets for the current drag within a single display. */ public class DragLayout extends LinearLayout - implements ViewTreeObserver.OnComputeInternalInsetsListener { + implements ViewTreeObserver.OnComputeInternalInsetsListener, DragLayoutProvider { // While dragging the status bar is hidden. private static final int HIDE_STATUS_BAR_FLAGS = StatusBarManager.DISABLE_NOTIFICATION_ICONS @@ -78,7 +81,7 @@ public class DragLayout extends LinearLayout | StatusBarManager.DISABLE_CLOCK | StatusBarManager.DISABLE_SYSTEM_INFO; - private final DragAndDropPolicy mPolicy; + private final DropTarget mPolicy; private final SplitScreenController mSplitScreenController; private final IconProvider mIconProvider; private final StatusBarManager mStatusBarManager; @@ -89,7 +92,7 @@ public class DragLayout extends LinearLayout // Whether the device is currently in left/right split mode private boolean mIsLeftRightSplit; - private DragAndDropPolicy.Target mCurrentTarget = null; + private SplitDragPolicy.Target mCurrentTarget = null; private DropZoneView mDropZoneView1; private DropZoneView mDropZoneView2; @@ -102,6 +105,8 @@ public class DragLayout extends LinearLayout private boolean mIsShowing; private boolean mHasDropped; private DragSession mSession; + // The last position that was handled by the drag layout + private final Point mLastPosition = new Point(); @SuppressLint("WrongConstant") public DragLayout(Context context, SplitScreenController splitScreenController, @@ -109,7 +114,7 @@ public class DragLayout extends LinearLayout super(context); mSplitScreenController = splitScreenController; mIconProvider = iconProvider; - mPolicy = new DragAndDropPolicy(context, splitScreenController); + mPolicy = new SplitDragPolicy(context, splitScreenController); mStatusBarManager = context.getSystemService(StatusBarManager.class); mLastConfiguration.setTo(context.getResources().getConfiguration()); @@ -265,6 +270,15 @@ public class DragLayout extends LinearLayout */ public void prepare(DragSession session, InstanceId loggerSessionId) { mPolicy.start(session, loggerSessionId); + updateSession(session); + } + + /** + * Updates the drag layout based on the diven drag session. + */ + public void updateSession(DragSession session) { + // Note: The policy currently just keeps a reference to the session + boolean updatingExistingSession = mSession != null; mSession = session; mHasDropped = false; mCurrentTarget = null; @@ -277,9 +291,11 @@ public class DragLayout extends LinearLayout final int activityType = taskInfo1.getActivityType(); if (activityType == ACTIVITY_TYPE_STANDARD) { Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo); - int bgColor1 = getResizingBackgroundColor(taskInfo1).toArgb(); + int bgColor1 = getResizingBackgroundColor(taskInfo1); mDropZoneView1.setAppInfo(bgColor1, icon1); mDropZoneView2.setAppInfo(bgColor1, icon1); + mDropZoneView1.setForceIgnoreBottomMargin(false); + mDropZoneView2.setForceIgnoreBottomMargin(false); updateDropZoneSizes(null, null); // passing null splits the views evenly } else { // We use the first drop zone to show the fullscreen highlight, and don't need @@ -297,10 +313,10 @@ public class DragLayout extends LinearLayout mSplitScreenController.getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT); if (topOrLeftTask != null && bottomOrRightTask != null) { Drawable topOrLeftIcon = mIconProvider.getIcon(topOrLeftTask.topActivityInfo); - int topOrLeftColor = getResizingBackgroundColor(topOrLeftTask).toArgb(); + int topOrLeftColor = getResizingBackgroundColor(topOrLeftTask); Drawable bottomOrRightIcon = mIconProvider.getIcon( bottomOrRightTask.topActivityInfo); - int bottomOrRightColor = getResizingBackgroundColor(bottomOrRightTask).toArgb(); + int bottomOrRightColor = getResizingBackgroundColor(bottomOrRightTask); mDropZoneView1.setAppInfo(topOrLeftColor, topOrLeftIcon); mDropZoneView2.setAppInfo(bottomOrRightColor, bottomOrRightIcon); } @@ -312,6 +328,11 @@ public class DragLayout extends LinearLayout updateDropZoneSizes(topOrLeftBounds, bottomOrRightBounds); } requestLayout(); + if (updatingExistingSession) { + // Update targets if we are already currently dragging + recomputeDropTargets(); + update(mLastPosition.x, mLastPosition.y); + } } private void updateDropZoneSizesForSingleTask() { @@ -359,11 +380,21 @@ public class DragLayout extends LinearLayout mDropZoneView2.setLayoutParams(dropZoneView2); } + /** + * Shows the drag layout. + */ public void show() { mIsShowing = true; recomputeDropTargets(); } + @NonNull + @Override + public void addDraggingView(ViewGroup rootView) { + // TODO(b/349828130) We need to separate out view + logic here + rootView.addView(this, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + } + /** * Recalculates the drop targets based on the current policy. */ @@ -371,9 +402,9 @@ public class DragLayout extends LinearLayout if (!mIsShowing) { return; } - final ArrayList<DragAndDropPolicy.Target> targets = mPolicy.getTargets(mInsets); + final List<SplitDragPolicy.Target> targets = mPolicy.getTargets(mInsets); for (int i = 0; i < targets.size(); i++) { - final DragAndDropPolicy.Target target = targets.get(i); + final SplitDragPolicy.Target target = targets.get(i); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Add target: %s", target); // Inset the draw region by a little bit target.drawRegion.inset(mDisplayMargin, mDisplayMargin); @@ -384,13 +415,19 @@ public class DragLayout extends LinearLayout * Updates the visible drop target as the user drags. */ public void update(DragEvent event) { + update((int) event.getX(), (int) event.getY()); + } + + /** + * Updates the visible drop target as the user drags to the given coordinates. + */ + private void update(int x, int y) { if (mHasDropped) { return; } // Find containing region, if the same as mCurrentRegion, then skip, otherwise, animate the // visibility of the current region - DragAndDropPolicy.Target target = mPolicy.getTargetAtLocation( - (int) event.getX(), (int) event.getY()); + SplitDragPolicy.Target target = mPolicy.getTargetAtLocation(x, y); if (mCurrentTarget != target) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Current target: %s", target); if (target == null) { @@ -429,6 +466,7 @@ public class DragLayout extends LinearLayout } mCurrentTarget = target; } + mLastPosition.set(x, y); } /** @@ -436,6 +474,7 @@ public class DragLayout extends LinearLayout */ public void hide(DragEvent event, Runnable hideCompleteCallback) { mIsShowing = false; + mLastPosition.set(-1, -1); animateSplitContainers(false, () -> { if (hideCompleteCallback != null) { hideCompleteCallback.run(); @@ -456,13 +495,13 @@ public class DragLayout extends LinearLayout /** * Handles the drop onto a target and animates out the visible drop targets. */ - public boolean drop(DragEvent event, SurfaceControl dragSurface, - Runnable dropCompleteCallback) { + public boolean drop(DragEvent event, @NonNull SurfaceControl dragSurface, + @Nullable WindowContainerToken hideTaskToken, Runnable dropCompleteCallback) { final boolean handledDrop = mCurrentTarget != null; mHasDropped = true; // Process the drop - mPolicy.handleDrop(mCurrentTarget); + mPolicy.onDropped(mCurrentTarget, hideTaskToken); // Start animating the drop UI out with the drag surface hide(event, dropCompleteCallback); @@ -472,7 +511,7 @@ public class DragLayout extends LinearLayout return handledDrop; } - private void hideDragSurface(SurfaceControl dragSurface) { + private void hideDragSurface(@NonNull SurfaceControl dragSurface) { final SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); final ValueAnimator dragSurfaceAnimator = ValueAnimator.ofFloat(0f, 1f); // Currently the splash icon animation runs with the default ValueAnimator duration of @@ -545,7 +584,7 @@ public class DragLayout extends LinearLayout } } - private void animateHighlight(DragAndDropPolicy.Target target) { + private void animateHighlight(SplitDragPolicy.Target target) { if (target.type == TYPE_SPLIT_LEFT || target.type == TYPE_SPLIT_TOP) { mDropZoneView1.setShowingHighlight(true); mDropZoneView2.setShowingHighlight(false); @@ -555,6 +594,11 @@ public class DragLayout extends LinearLayout } } + private static int getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) { + final int taskBgColor = taskInfo.taskDescription.getBackgroundColor(); + return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb(); + } + /** * Dumps information about this drag layout. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayoutProvider.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayoutProvider.kt new file mode 100644 index 000000000000..3d408242f5f8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayoutProvider.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 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 android.content.res.Configuration +import android.view.DragEvent +import android.view.SurfaceControl +import android.view.ViewGroup +import android.window.WindowContainerToken +import com.android.internal.logging.InstanceId +import java.io.PrintWriter + +/** Interface to be implemented by any controllers providing a layout for DragAndDrop in Shell */ +interface DragLayoutProvider { + /** + * Updates the drag layout based on the given drag session. + */ + fun updateSession(session: DragSession) + /** + * Called when a new drag is started. + */ + fun prepare(session: DragSession, loggerSessionId: InstanceId?) + + /** + * Shows the drag layout. + */ + fun show() + + /** + * Updates the visible drop target as the user drags. + */ + fun update(event: DragEvent?) + + /** + * Hides the drag layout and animates out the visible drop targets. + */ + fun hide(event: DragEvent?, hideCompleteCallback: Runnable?) + + /** + * Whether target has already been dropped or not + */ + fun hasDropped(): Boolean + + /** + * Handles the drop onto a target and animates out the visible drop targets. + */ + fun drop( + event: DragEvent?, dragSurface: SurfaceControl, + hideTaskToken: WindowContainerToken?, dropCompleteCallback: Runnable? + ): Boolean + + /** + * Dumps information about this drag layout. + */ + fun dump(pw: PrintWriter, prefix: String?) + + /** + * @return a View which will be added to the global root view for drag and drop + */ + fun addDraggingView(viewGroup: ViewGroup) + + /** + * Called when the configuration changes. + */ + fun onConfigChanged(newConfig: Configuration?) +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java index 0addd432aff0..dcbdfa349687 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java @@ -18,6 +18,7 @@ package com.android.wm.shell.draganddrop; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.content.ClipDescription.EXTRA_HIDE_DRAG_SOURCE_TASK_ID; import android.app.ActivityManager; import android.app.ActivityTaskManager; @@ -27,10 +28,13 @@ import android.content.ClipData; import android.content.ClipDescription; import android.content.Intent; import android.content.pm.ActivityInfo; +import android.os.PersistableBundle; import androidx.annotation.Nullable; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.List; @@ -61,6 +65,7 @@ public class DragSession { @WindowConfiguration.ActivityType int runningTaskActType = ACTIVITY_TYPE_STANDARD; boolean dragItemSupportsSplitscreen; + int hideDragSourceTaskId = -1; DragSession(ActivityTaskManager activityTaskManager, DisplayLayout dispLayout, ClipData data, int dragFlags) { @@ -68,6 +73,11 @@ public class DragSession { mInitialDragData = data; mInitialDragFlags = dragFlags; displayLayout = dispLayout; + hideDragSourceTaskId = data.getDescription().getExtras() != null + ? data.getDescription().getExtras().getInt(EXTRA_HIDE_DRAG_SOURCE_TASK_ID, -1) + : -1; + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Extracting drag source taskId: taskId=%d", hideDragSourceTaskId); } /** @@ -79,17 +89,38 @@ public class DragSession { } /** - * Updates the session data based on the current state of the system. + * Updates the running task for this drag session. */ - void update() { - List<ActivityManager.RunningTaskInfo> tasks = - mActivityTaskManager.getTasks(1, false /* filterOnlyVisibleRecents */); + void updateRunningTask() { + final boolean hideDragSourceTask = hideDragSourceTaskId != -1; + final List<ActivityManager.RunningTaskInfo> tasks = + mActivityTaskManager.getTasks(hideDragSourceTask ? 2 : 1, + false /* filterOnlyVisibleRecents */); if (!tasks.isEmpty()) { - final ActivityManager.RunningTaskInfo task = tasks.get(0); - runningTaskInfo = task; - runningTaskWinMode = task.getWindowingMode(); - runningTaskActType = task.getActivityType(); + for (int i = tasks.size() - 1; i >= 0; i--) { + final ActivityManager.RunningTaskInfo task = tasks.get(i); + if (hideDragSourceTask && hideDragSourceTaskId == task.taskId) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Skipping running task: id=%d component=%s", task.taskId, + task.baseIntent != null ? task.baseIntent.getComponent() : "null"); + continue; + } + runningTaskInfo = task; + runningTaskWinMode = task.getWindowingMode(); + runningTaskActType = task.getActivityType(); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Running task: id=%d component=%s", task.taskId, + task.baseIntent != null ? task.baseIntent.getComponent() : "null"); + break; + } } + } + + /** + * Updates the session data based on the current state of the system at the start of the drag. + */ + void initialize() { + updateRunningTask(); activityInfo = mInitialDragData.getItemAt(0).getActivityInfo(); // TODO: This should technically check & respect config_supportsNonResizableMultiWindow diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java index e215870f1894..22cfa328bfda 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java @@ -19,16 +19,28 @@ 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 static android.view.View.DRAG_FLAG_ACCESSIBILITY_ACTION; +import static android.view.View.DRAG_FLAG_GLOBAL; +import static android.view.View.DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION; +import static android.view.View.DRAG_FLAG_GLOBAL_PREFIX_URI_PERMISSION; +import static android.view.View.DRAG_FLAG_GLOBAL_SAME_APPLICATION; +import static android.view.View.DRAG_FLAG_GLOBAL_URI_READ; +import static android.view.View.DRAG_FLAG_GLOBAL_URI_WRITE; +import static android.view.View.DRAG_FLAG_HIDE_CALLING_TASK_ON_DRAG_START; +import static android.view.View.DRAG_FLAG_OPAQUE; +import static android.view.View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION; +import static android.view.View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG; import android.app.PendingIntent; import android.content.ClipData; import android.content.ClipDescription; import android.view.DragEvent; -import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.StringJoiner; + /** Collection of utility classes for handling drag and drop. */ public class DragUtils { private static final String TAG = "DragUtils"; @@ -76,7 +88,7 @@ public class DragUtils { */ @Nullable public static PendingIntent getLaunchIntent(@NonNull ClipData data, int dragFlags) { - if ((dragFlags & View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG) == 0) { + if ((dragFlags & DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG) == 0) { // Disallow launching the intent if the app does not want to delegate it to the system return null; } @@ -105,4 +117,35 @@ public class DragUtils { } return mimeTypes; } + + /** + * Returns the string description of the given {@param dragFlags}. + */ + public static String dragFlagsToString(int dragFlags) { + StringJoiner str = new StringJoiner("|"); + if ((dragFlags & DRAG_FLAG_GLOBAL) != 0) { + str.add("GLOBAL"); + } else if ((dragFlags & DRAG_FLAG_GLOBAL_URI_READ) != 0) { + str.add("GLOBAL_URI_READ"); + } else if ((dragFlags & DRAG_FLAG_GLOBAL_URI_WRITE) != 0) { + str.add("GLOBAL_URI_WRITE"); + } else if ((dragFlags & DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION) != 0) { + str.add("GLOBAL_PERSISTABLE_URI_PERMISSION"); + } else if ((dragFlags & DRAG_FLAG_GLOBAL_PREFIX_URI_PERMISSION) != 0) { + str.add("GLOBAL_PREFIX_URI_PERMISSION"); + } else if ((dragFlags & DRAG_FLAG_OPAQUE) != 0) { + str.add("OPAQUE"); + } else if ((dragFlags & DRAG_FLAG_ACCESSIBILITY_ACTION) != 0) { + str.add("ACCESSIBILITY_ACTION"); + } else if ((dragFlags & DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION) != 0) { + str.add("REQUEST_SURFACE_FOR_RETURN_ANIMATION"); + } else if ((dragFlags & DRAG_FLAG_GLOBAL_SAME_APPLICATION) != 0) { + str.add("GLOBAL_SAME_APPLICATION"); + } else if ((dragFlags & DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG) != 0) { + str.add("START_INTENT_SENDER_ON_UNHANDLED_DRAG"); + } else if ((dragFlags & DRAG_FLAG_HIDE_CALLING_TASK_ON_DRAG_START) != 0) { + str.add("HIDE_CALLING_TASK_ON_DRAG_START"); + } + return str.toString(); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropTarget.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropTarget.kt new file mode 100644 index 000000000000..122a105dbf8d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropTarget.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 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 android.graphics.Insets +import android.window.WindowContainerToken +import com.android.internal.logging.InstanceId + +/** + * Interface to be implemented by classes which want to provide drop targets + * for DragAndDrop in Shell + */ +interface DropTarget { + // TODO(b/349828130) Delete after flexible split launches + /** + * Called at the start of a Drag, before input events are processed. + */ + fun start(dragSession: DragSession, logSessionId: InstanceId) + /** + * @return [SplitDragPolicy.Target] corresponding to the given coords in display bounds. + */ + fun getTargetAtLocation(x: Int, y: Int) : SplitDragPolicy.Target + /** + * @return total number of drop targets for the current drag session. + */ + fun getNumTargets() : Int + // TODO(b/349828130) + + /** + * @return [List<SplitDragPolicy.Target>] to show for the current drag session. + */ + fun getTargets(insets: Insets) : List<SplitDragPolicy.Target> + /** + * Called when user is hovering Drag object over the given Target + */ + fun onHoveringOver(target: SplitDragPolicy.Target) {} + /** + * Called when the user has dropped the provided target (need not be the same target as + * [onHoveringOver]) + */ + fun onDropped(target: SplitDragPolicy.Target, hideTaskToken: WindowContainerToken) +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java index 724a130ef52d..f9749ec1e2b7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java @@ -16,7 +16,7 @@ package com.android.wm.shell.draganddrop; -import static com.android.wm.shell.animation.Interpolators.FAST_OUT_SLOW_IN; +import static com.android.wm.shell.shared.animation.Interpolators.FAST_OUT_SLOW_IN; import android.animation.Animator; import android.animation.ObjectAnimator; @@ -37,13 +37,16 @@ import android.widget.ImageView; import androidx.annotation.Nullable; import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; +import com.android.wm.shell.protolog.ShellProtoLogGroup; /** * Renders a drop zone area for items being dragged. */ public class DropZoneView extends FrameLayout { + private static final boolean DEBUG_LAYOUT = false; private static final float SPLASHSCREEN_ALPHA = 0.90f; private static final float HIGHLIGHT_ALPHA = 1f; private static final int MARGIN_ANIMATION_ENTER_DURATION = 400; @@ -77,6 +80,7 @@ public class DropZoneView extends FrameLayout { private int mHighlightColor; private ObjectAnimator mBackgroundAnimator; + private int mTargetBackgroundColor; private ObjectAnimator mMarginAnimator; private float mMarginPercent; @@ -146,6 +150,10 @@ public class DropZoneView extends FrameLayout { /** Ignores the bottom margin provided by the insets. */ public void setForceIgnoreBottomMargin(boolean ignoreBottomMargin) { + if (DEBUG_LAYOUT) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "setForceIgnoreBottomMargin: ignore=%b", ignoreBottomMargin); + } mIgnoreBottomMargin = ignoreBottomMargin; if (mMarginPercent > 0) { mMarginView.invalidate(); @@ -154,8 +162,14 @@ public class DropZoneView extends FrameLayout { /** Sets the bottom inset so the drop zones are above bottom navigation. */ public void setBottomInset(float bottom) { + if (DEBUG_LAYOUT) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "setBottomInset: inset=%f", + bottom); + } mBottomInset = bottom; - ((LayoutParams) mSplashScreenView.getLayoutParams()).bottomMargin = (int) bottom; + final LayoutParams lp = (LayoutParams) mSplashScreenView.getLayoutParams(); + lp.bottomMargin = (int) bottom; + mSplashScreenView.setLayoutParams(lp); if (mMarginPercent > 0) { mMarginView.invalidate(); } @@ -181,6 +195,9 @@ public class DropZoneView extends FrameLayout { /** Animates between highlight and splashscreen depending on current state. */ public void animateSwitch() { + if (DEBUG_LAYOUT) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "animateSwitch"); + } mShowingHighlight = !mShowingHighlight; mShowingSplash = !mShowingHighlight; final int newColor = mShowingHighlight ? mHighlightColor : mSplashScreenColor; @@ -190,6 +207,10 @@ public class DropZoneView extends FrameLayout { /** Animates the highlight indicating the zone is hovered on or not. */ public void setShowingHighlight(boolean showingHighlight) { + if (DEBUG_LAYOUT) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "setShowingHighlight: showing=%b", + showingHighlight); + } mShowingHighlight = showingHighlight; mShowingSplash = !mShowingHighlight; final int newColor = mShowingHighlight ? mHighlightColor : mSplashScreenColor; @@ -199,6 +220,10 @@ public class DropZoneView extends FrameLayout { /** Animates the margins around the drop zone to show or hide. */ public void setShowingMargin(boolean visible) { + if (DEBUG_LAYOUT) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "setShowingMargin: visible=%b", + visible); + } if (mShowingMargin != visible) { mShowingMargin = visible; animateMarginToState(); @@ -212,6 +237,15 @@ public class DropZoneView extends FrameLayout { } private void animateBackground(int startColor, int endColor) { + if (DEBUG_LAYOUT) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "animateBackground: start=%s end=%s", + Integer.toHexString(startColor), Integer.toHexString(endColor)); + } + if (endColor == mTargetBackgroundColor) { + // Already at, or animating to, that background color + return; + } if (mBackgroundAnimator != null) { mBackgroundAnimator.cancel(); } @@ -223,6 +257,7 @@ public class DropZoneView extends FrameLayout { mBackgroundAnimator.setInterpolator(FAST_OUT_SLOW_IN); } mBackgroundAnimator.start(); + mTargetBackgroundColor = endColor; } private void animateSplashScreenIcon() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt index 31214eba8dd0..ffcfe6447e2d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt @@ -25,7 +25,7 @@ import android.view.IWindowManager import android.window.IGlobalDragListener import android.window.IUnhandledDragCallback import androidx.annotation.VisibleForTesting -import com.android.internal.protolog.common.ProtoLog +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.protolog.ShellProtoLogGroup import java.util.function.Consumer diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java index a42ca1905ee7..2a19d6512b56 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java @@ -16,7 +16,7 @@ package com.android.wm.shell.draganddrop; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; @@ -32,15 +32,15 @@ import static android.content.Intent.EXTRA_USER; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; -import static com.android.wm.shell.draganddrop.DragAndDropConstants.EXTRA_DISALLOW_HIT_REGION; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_FULLSCREEN; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_FULLSCREEN; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_BOTTOM; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_LEFT; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_RIGHT; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_TOP; +import static com.android.wm.shell.shared.draganddrop.DragAndDropConstants.EXTRA_DISALLOW_HIT_REGION; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import android.app.ActivityOptions; import android.app.ActivityTaskManager; @@ -59,6 +59,7 @@ import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; import android.util.Slog; +import android.window.WindowContainerToken; import androidx.annotation.IntDef; import androidx.annotation.NonNull; @@ -66,10 +67,10 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.internal.logging.InstanceId; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; -import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.splitscreen.SplitScreenController; import java.lang.annotation.Retention; @@ -79,34 +80,39 @@ import java.util.ArrayList; /** * The policy for handling drag and drop operations to shell. */ -public class DragAndDropPolicy { +public class SplitDragPolicy implements DropTarget { - private static final String TAG = DragAndDropPolicy.class.getSimpleName(); + private static final String TAG = SplitDragPolicy.class.getSimpleName(); private final Context mContext; - private final Starter mStarter; + // Used only for launching a fullscreen task (or as a fallback if there is no split starter) + private final Starter mFullscreenStarter; + // Used for launching tasks into splitscreen + private final Starter mSplitscreenStarter; private final SplitScreenController mSplitScreen; - private final ArrayList<DragAndDropPolicy.Target> mTargets = new ArrayList<>(); + private final ArrayList<SplitDragPolicy.Target> mTargets = new ArrayList<>(); private final RectF mDisallowHitRegion = new RectF(); private InstanceId mLoggerSessionId; private DragSession mSession; - public DragAndDropPolicy(Context context, SplitScreenController splitScreen) { + public SplitDragPolicy(Context context, SplitScreenController splitScreen) { this(context, splitScreen, new DefaultStarter(context)); } @VisibleForTesting - DragAndDropPolicy(Context context, SplitScreenController splitScreen, Starter starter) { + SplitDragPolicy(Context context, SplitScreenController splitScreen, + Starter fullscreenStarter) { mContext = context; mSplitScreen = splitScreen; - mStarter = mSplitScreen != null ? mSplitScreen : starter; + mFullscreenStarter = fullscreenStarter; + mSplitscreenStarter = splitScreen; } /** * Starts a new drag session with the given initial drag data. */ - void start(DragSession session, InstanceId loggerSessionId) { + public void start(DragSession session, InstanceId loggerSessionId) { mLoggerSessionId = loggerSessionId; mSession = session; RectF disallowHitRegion = mSession.appData != null @@ -122,7 +128,8 @@ public class DragAndDropPolicy { /** * Returns the number of targets. */ - int getNumTargets() { + @Override + public int getNumTargets() { return mTargets.size(); } @@ -130,7 +137,8 @@ public class DragAndDropPolicy { * Returns the target's regions based on the current state of the device and display. */ @NonNull - ArrayList<Target> getTargets(Insets insets) { + @Override + public ArrayList<Target> getTargets(@NonNull Insets insets) { mTargets.clear(); if (mSession == null) { // Return early if this isn't an app drag @@ -216,12 +224,12 @@ public class DragAndDropPolicy { * Returns the target at the given position based on the targets previously calculated. */ @Nullable - Target getTargetAtLocation(int x, int y) { + public Target getTargetAtLocation(int x, int y) { if (mDisallowHitRegion.contains(x, y)) { return null; } for (int i = mTargets.size() - 1; i >= 0; i--) { - DragAndDropPolicy.Target t = mTargets.get(i); + SplitDragPolicy.Target t = mTargets.get(i); if (t.hitRegion.contains(x, y)) { return t; } @@ -229,8 +237,13 @@ public class DragAndDropPolicy { return null; } + /** + * Handles the drop on a given {@param target}. If a {@param hideTaskToken} is set, then the + * handling of the drop will attempt to hide the given task as a part of the same window + * container transaction if possible. + */ @VisibleForTesting - void handleDrop(Target target) { + public void onDropped(Target target, @Nullable WindowContainerToken hideTaskToken) { if (target == null || !mTargets.contains(target)) { return; } @@ -245,17 +258,21 @@ public class DragAndDropPolicy { mSplitScreen.onDroppedToSplit(position, mLoggerSessionId); } + final Starter starter = target.type == TYPE_FULLSCREEN + ? mFullscreenStarter + : mSplitscreenStarter; if (mSession.appData != null) { - launchApp(mSession, position); + launchApp(mSession, starter, position, hideTaskToken); } else { - launchIntent(mSession, position); + launchIntent(mSession, starter, position, hideTaskToken); } } /** * Launches an app provided by SysUI. */ - private void launchApp(DragSession session, @SplitPosition int position) { + private void launchApp(DragSession session, Starter starter, @SplitPosition int position, + @Nullable WindowContainerToken hideTaskToken) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Launching app data at position=%d", position); final ClipDescription description = session.getClipDescription(); @@ -265,8 +282,7 @@ public class DragAndDropPolicy { baseActivityOpts.setDisallowEnterPictureInPictureWhileLaunching(true); // Put BAL flags to avoid activity start aborted. baseActivityOpts.setPendingIntentBackgroundActivityStartMode( - MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - baseActivityOpts.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); final Bundle opts = baseActivityOpts.toBundle(); if (session.appData.hasExtra(EXTRA_ACTIVITY_OPTIONS)) { opts.putAll(session.appData.getBundleExtra(EXTRA_ACTIVITY_OPTIONS)); @@ -275,11 +291,15 @@ public class DragAndDropPolicy { if (isTask) { final int taskId = session.appData.getIntExtra(EXTRA_TASK_ID, INVALID_TASK_ID); - mStarter.startTask(taskId, position, opts); + starter.startTask(taskId, position, opts, hideTaskToken); } else if (isShortcut) { + if (hideTaskToken != null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Can not hide task token with starting shortcut"); + } final String packageName = session.appData.getStringExtra(EXTRA_PACKAGE_NAME); final String id = session.appData.getStringExtra(EXTRA_SHORTCUT_ID); - mStarter.startShortcut(packageName, id, position, opts, user); + starter.startShortcut(packageName, id, position, opts, user); } else { final PendingIntent launchIntent = session.appData.getParcelableExtra(EXTRA_PENDING_INTENT); @@ -288,15 +308,16 @@ public class DragAndDropPolicy { Log.e(TAG, "Expected app intent's EXTRA_USER to match pending intent user"); } } - mStarter.startIntent(launchIntent, user.getIdentifier(), null /* fillIntent */, - position, opts); + starter.startIntent(launchIntent, user.getIdentifier(), null /* fillIntent */, + position, opts, hideTaskToken); } } /** * Launches an intent sender provided by an application. */ - private void launchIntent(DragSession session, @SplitPosition int position) { + private void launchIntent(DragSession session, Starter starter, @SplitPosition int position, + @Nullable WindowContainerToken hideTaskToken) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Launching intent at position=%d", position); final ActivityOptions baseActivityOpts = ActivityOptions.makeBasic(); @@ -309,20 +330,22 @@ public class DragAndDropPolicy { | FLAG_ACTIVITY_MULTIPLE_TASK); final Bundle opts = baseActivityOpts.toBundle(); - mStarter.startIntent(session.launchableIntent, + starter.startIntent(session.launchableIntent, session.launchableIntent.getCreatorUserHandle().getIdentifier(), - null /* fillIntent */, position, opts); + null /* fillIntent */, position, opts, hideTaskToken); } /** * Interface for actually committing the task launches. */ public interface Starter { - void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options); + void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options, + @Nullable WindowContainerToken hideTaskToken); void startShortcut(String packageName, String shortcutId, @SplitPosition int position, @Nullable Bundle options, UserHandle user); void startIntent(PendingIntent intent, int userId, Intent fillInIntent, - @SplitPosition int position, @Nullable Bundle options); + @SplitPosition int position, @Nullable Bundle options, + @Nullable WindowContainerToken hideTaskToken); void enterSplitScreen(int taskId, boolean leftOrTop); /** @@ -344,7 +367,12 @@ public class DragAndDropPolicy { } @Override - public void startTask(int taskId, int position, @Nullable Bundle options) { + public void startTask(int taskId, int position, @Nullable Bundle options, + @Nullable WindowContainerToken hideTaskToken) { + if (hideTaskToken != null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Default starter does not support hide task token"); + } try { ActivityTaskManager.getService().startActivityFromRecents(taskId, options); } catch (RemoteException e) { @@ -367,7 +395,12 @@ public class DragAndDropPolicy { @Override public void startIntent(PendingIntent intent, int userId, @Nullable Intent fillInIntent, - int position, @Nullable Bundle options) { + int position, @Nullable Bundle options, + @Nullable WindowContainerToken hideTaskToken) { + if (hideTaskToken != null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Default starter does not support hide task token"); + } try { intent.send(mContext, 0, fillInIntent, null, null, null, options); } catch (PendingIntent.CanceledException e) { @@ -388,8 +421,9 @@ public class DragAndDropPolicy { /** * Represents a drop target. + * TODO(b/349828130): Move this into {@link DropTarget} */ - static class Target { + public static class Target { static final int TYPE_FULLSCREEN = 0; static final int TYPE_SPLIT_LEFT = 1; static final int TYPE_SPLIT_TOP = 2; @@ -420,7 +454,7 @@ public class DragAndDropPolicy { @Override public String toString() { - return "Target {hit=" + hitRegion + " draw=" + drawRegion + "}"; + return "Target {type=" + type + " hit=" + hitRegion + " draw=" + drawRegion + "}"; } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index 7d2aa275a684..83cc18baf6cc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -25,11 +25,12 @@ import android.content.Context; import android.util.SparseArray; import android.view.SurfaceControl; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import com.android.wm.shell.shared.DesktopModeStatus; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.WindowDecorViewModel; @@ -49,6 +50,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, private final ShellTaskOrganizer mShellTaskOrganizer; private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository; private final WindowDecorViewModel mWindowDecorationViewModel; + private final LaunchAdjacentController mLaunchAdjacentController; private final SparseArray<State> mTasks = new SparseArray<>(); @@ -62,11 +64,13 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, ShellInit shellInit, ShellTaskOrganizer shellTaskOrganizer, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, + LaunchAdjacentController launchAdjacentController, WindowDecorViewModel windowDecorationViewModel) { mContext = context; mShellTaskOrganizer = shellTaskOrganizer; mWindowDecorationViewModel = windowDecorationViewModel; mDesktopModeTaskRepository = desktopModeTaskRepository; + mLaunchAdjacentController = launchAdjacentController; if (shellInit != null) { shellInit.addInitCallback(this::onInit, this); } @@ -99,17 +103,14 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, if (DesktopModeStatus.canEnterDesktopMode(mContext)) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.addOrMoveFreeformTaskToTop(taskInfo.displayId, taskInfo.taskId); - repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId); if (taskInfo.isVisible) { - if (repository.addActiveTask(taskInfo.displayId, taskInfo.taskId)) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "Adding active freeform task: #%d", taskInfo.taskId); - } - repository.updateVisibleFreeformTasks(taskInfo.displayId, taskInfo.taskId, - true); + repository.addActiveTask(taskInfo.displayId, taskInfo.taskId); + repository.updateTaskVisibility(taskInfo.displayId, taskInfo.taskId, + /* visible= */ true); } }); } + updateLaunchAdjacentController(); } @Override @@ -121,18 +122,13 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, if (DesktopModeStatus.canEnterDesktopMode(mContext)) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId); - repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId); - if (repository.removeActiveTask(taskInfo.taskId)) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "Removing active freeform task: #%d", taskInfo.taskId); - } - repository.updateVisibleFreeformTasks(taskInfo.displayId, taskInfo.taskId, false); }); } mWindowDecorationViewModel.onTaskVanished(taskInfo); if (!Transitions.ENABLE_SHELL_TRANSITIONS) { mWindowDecorationViewModel.destroyWindowDecoration(taskInfo); } + updateLaunchAdjacentController(); } @Override @@ -146,15 +142,25 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, if (DesktopModeStatus.canEnterDesktopMode(mContext)) { mDesktopModeTaskRepository.ifPresent(repository -> { if (taskInfo.isVisible) { - if (repository.addActiveTask(taskInfo.displayId, taskInfo.taskId)) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "Adding active freeform task: #%d", taskInfo.taskId); - } + repository.addActiveTask(taskInfo.displayId, taskInfo.taskId); + } else if (repository.isClosingTask(taskInfo.taskId)) { + repository.removeClosingTask(taskInfo.taskId); } - repository.updateVisibleFreeformTasks(taskInfo.displayId, taskInfo.taskId, + repository.updateTaskVisibility(taskInfo.displayId, taskInfo.taskId, taskInfo.isVisible); }); } + updateLaunchAdjacentController(); + } + + private void updateLaunchAdjacentController() { + for (int i = 0; i < mTasks.size(); i++) { + if (mTasks.valueAt(i).mTaskInfo.isVisible) { + mLaunchAdjacentController.setLaunchAdjacentEnabled(false); + return; + } + } + mLaunchAdjacentController.setLaunchAdjacentEnabled(true); } @Override @@ -168,7 +174,6 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, if (DesktopModeStatus.canEnterDesktopMode(mContext) && taskInfo.isFocused) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.addOrMoveFreeformTaskToTop(taskInfo.displayId, taskInfo.taskId); - repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId); }); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java index 84027753435b..517e20910f6d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java @@ -19,6 +19,8 @@ package com.android.wm.shell.freeform; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; @@ -26,6 +28,7 @@ import android.app.ActivityManager; import android.app.WindowConfiguration; import android.content.Context; import android.graphics.Rect; +import android.os.Handler; import android.os.IBinder; import android.util.ArrayMap; import android.view.SurfaceControl; @@ -37,8 +40,11 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.WindowDecorViewModel; @@ -56,9 +62,13 @@ public class FreeformTaskTransitionHandler private final Context mContext; private final Transitions mTransitions; private final WindowDecorViewModel mWindowDecorViewModel; + private final DesktopModeTaskRepository mDesktopModeTaskRepository; private final DisplayController mDisplayController; + private final InteractionJankMonitor mInteractionJankMonitor; private final ShellExecutor mMainExecutor; private final ShellExecutor mAnimExecutor; + @ShellMainThread + private final Handler mHandler; private final List<IBinder> mPendingTransitionTokens = new ArrayList<>(); @@ -71,13 +81,19 @@ public class FreeformTaskTransitionHandler WindowDecorViewModel windowDecorViewModel, DisplayController displayController, ShellExecutor mainExecutor, - ShellExecutor animExecutor) { + ShellExecutor animExecutor, + DesktopModeTaskRepository desktopModeTaskRepository, + InteractionJankMonitor interactionJankMonitor, + @ShellMainThread Handler handler) { mTransitions = transitions; mContext = context; mWindowDecorViewModel = windowDecorViewModel; + mDesktopModeTaskRepository = desktopModeTaskRepository; mDisplayController = displayController; + mInteractionJankMonitor = interactionJankMonitor; mMainExecutor = mainExecutor; mAnimExecutor = animExecutor; + mHandler = handler; if (Transitions.ENABLE_SHELL_TRANSITIONS) { shellInit.addInitCallback(this::onInit, this); } @@ -107,9 +123,11 @@ public class FreeformTaskTransitionHandler } @Override - public void startMinimizedModeTransition(WindowContainerTransaction wct) { - final int type = WindowManager.TRANSIT_TO_BACK; - mPendingTransitionTokens.add(mTransitions.startTransition(type, wct, this)); + public IBinder startMinimizedModeTransition(WindowContainerTransaction wct) { + final int type = Transitions.TRANSIT_MINIMIZE; + final IBinder token = mTransitions.startTransition(type, wct, this); + mPendingTransitionTokens.add(token); + return token; } @@ -149,7 +167,8 @@ public class FreeformTaskTransitionHandler transition, info.getType(), change); break; case WindowManager.TRANSIT_TO_BACK: - transitionHandled |= startMinimizeTransition(transition); + transitionHandled |= startMinimizeTransition( + transition, info.getType(), change); break; case WindowManager.TRANSIT_CLOSE: if (change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_FREEFORM) { @@ -215,8 +234,20 @@ public class FreeformTaskTransitionHandler return handled; } - private boolean startMinimizeTransition(IBinder transition) { - return mPendingTransitionTokens.contains(transition); + private boolean startMinimizeTransition( + IBinder transition, + int type, + TransitionInfo.Change change) { + if (!mPendingTransitionTokens.contains(transition)) { + return false; + } + + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (type != Transitions.TRANSIT_MINIMIZE) { + return false; + } + // TODO(b/361524575): Add minimize animations + return true; } private boolean startCloseTransition(IBinder transition, TransitionInfo.Change change, @@ -238,13 +269,22 @@ public class FreeformTaskTransitionHandler startBounds.top + (animation.getAnimatedFraction() * screenHeight)); t.apply(); }); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animations.remove(animator); - onAnimFinish.run(); - } - }); + if (mDesktopModeTaskRepository.getActiveNonMinimizedTaskCount( + change.getTaskInfo().displayId) == 1) { + // Starting the jank trace if closing the last window in desktop mode. + mInteractionJankMonitor.begin( + sc, mContext, mHandler, CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE); + } + animator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + animations.remove(animator); + onAnimFinish.run(); + mInteractionJankMonitor.end( + CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE); + } + }); animations.add(animator); return true; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java index 8da4c6ab4b36..ea68a694c3b9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java @@ -16,6 +16,7 @@ package com.android.wm.shell.freeform; +import android.os.IBinder; import android.window.WindowContainerTransaction; /** @@ -38,8 +39,9 @@ public interface FreeformTaskTransitionStarter { * * @param wct the {@link WindowContainerTransaction} that changes the windowing mode * + * @return the started transition */ - void startMinimizedModeTransition(WindowContainerTransaction wct); + IBinder startMinimizedModeTransition(WindowContainerTransaction wct); /** * Starts close window transition diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java index 2626e7380163..d2ceb67030fc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java @@ -27,7 +27,7 @@ import android.view.SurfaceControl; import androidx.annotation.NonNull; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.protolog.ShellProtoLogGroup; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java index cd478e5bd567..abec3b9c0c3b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java @@ -49,7 +49,7 @@ import android.window.TransitionRequestInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; @@ -170,7 +170,7 @@ public class KeyguardTransitionHandler @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull TransitionFinishCallback finishCallback) { - if (!handles(info) || mIsLaunchingActivityOverLockscreen) { + if (!handles(info)) { return false; } @@ -185,6 +185,9 @@ public class KeyguardTransitionHandler transition, info, startTransaction, finishTransaction, finishCallback); } + if (mIsLaunchingActivityOverLockscreen) { + return false; + } // Occlude/unocclude animations are only played if the keyguard is locked. if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java index 71cc8df80cad..422656c6d387 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java @@ -38,7 +38,6 @@ import android.view.IWindow; import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; -import android.view.SurfaceSession; import android.view.View; import android.view.WindowManager; import android.view.WindowlessWindowManager; @@ -105,7 +104,7 @@ public final class BackgroundWindowManager extends WindowlessWindowManager { @Override protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) { - final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + final SurfaceControl.Builder builder = new SurfaceControl.Builder() .setColorLayer() .setBufferSize(mDisplayBounds.width(), mDisplayBounds.height()) .setFormat(PixelFormat.RGB_888) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java index 962309f7c534..15472ebc149b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java @@ -23,7 +23,7 @@ import static com.android.wm.shell.onehanded.OneHandedState.STATE_ACTIVE; import static com.android.wm.shell.onehanded.OneHandedState.STATE_ENTERING; import static com.android.wm.shell.onehanded.OneHandedState.STATE_EXITING; import static com.android.wm.shell.onehanded.OneHandedState.STATE_NONE; -import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_ONE_HANDED; +import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_ONE_HANDED; import android.annotation.BinderThread; import android.content.ComponentName; @@ -55,6 +55,7 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.shared.annotations.ExternalThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.KeyguardChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -203,7 +204,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, DisplayController displayController, DisplayLayout displayLayout, TaskStackListenerImpl taskStackListener, InteractionJankMonitor jankMonitor, UiEventLogger uiEventLogger, - ShellExecutor mainExecutor, Handler mainHandler) { + ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler) { OneHandedSettingsUtil settingsUtil = new OneHandedSettingsUtil(); OneHandedAccessibilityUtil accessibilityUtil = new OneHandedAccessibilityUtil(context); OneHandedTimeoutHandler timeoutHandler = new OneHandedTimeoutHandler(mainExecutor); @@ -217,7 +218,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController>, mainExecutor); OneHandedDisplayAreaOrganizer organizer = new OneHandedDisplayAreaOrganizer( context, displayLayout, settingsUtil, animationController, tutorialHandler, - jankMonitor, mainExecutor); + jankMonitor, mainExecutor, mainHandler); OneHandedUiEventLogger oneHandedUiEventsLogger = new OneHandedUiEventLogger(uiEventLogger); return new OneHandedController(context, shellInit, shellCommandHandler, shellController, displayController, organizer, touchHandler, tutorialHandler, settingsUtil, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java index d157ca837608..95e633d0b5ec 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java @@ -23,6 +23,7 @@ import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSI import android.content.Context; import android.graphics.Rect; +import android.os.Handler; import android.os.SystemProperties; import android.text.TextUtils; import android.util.ArrayMap; @@ -42,6 +43,7 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.shared.annotations.ShellMainThread; import java.io.PrintWriter; import java.util.ArrayList; @@ -70,6 +72,8 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { private final OneHandedSettingsUtil mOneHandedSettingsUtil; private final InteractionJankMonitor mJankMonitor; private final Context mContext; + @ShellMainThread + private final Handler mHandler; private boolean mIsReady; private float mLastVisualOffset = 0; @@ -136,9 +140,11 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { OneHandedAnimationController animationController, OneHandedTutorialHandler tutorialHandler, InteractionJankMonitor jankMonitor, - ShellExecutor mainExecutor) { + ShellExecutor mainExecutor, + @ShellMainThread Handler handler) { super(mainExecutor); mContext = context; + mHandler = handler; setDisplayLayout(displayLayout); mOneHandedSettingsUtil = oneHandedSettingsUtil; mAnimationController = animationController; @@ -333,7 +339,7 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { getDisplayAreaTokenMap().entrySet().iterator().next(); final InteractionJankMonitor.Configuration.Builder builder = InteractionJankMonitor.Configuration.Builder.withSurface( - cujType, mContext, firstEntry.getValue()); + cujType, mContext, firstEntry.getValue(), mHandler); if (!TextUtils.isEmpty(tag)) { builder.setTag(tag); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java index ce98458c0575..93ede7a8b7aa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java @@ -16,7 +16,6 @@ package com.android.wm.shell.pip; -import android.content.ComponentName; import android.os.RemoteException; import android.view.IPinnedTaskListener; import android.view.WindowManagerGlobal; @@ -70,12 +69,6 @@ public class PinnedStackListenerForwarder { } } - private void onActivityHidden(ComponentName componentName) { - for (PinnedTaskListener listener : mListeners) { - listener.onActivityHidden(componentName); - } - } - @BinderThread private class PinnedTaskListenerImpl extends IPinnedTaskListener.Stub { @Override @@ -91,13 +84,6 @@ public class PinnedStackListenerForwarder { PinnedStackListenerForwarder.this.onImeVisibilityChanged(imeVisible, imeHeight); }); } - - @Override - public void onActivityHidden(ComponentName componentName) { - mMainExecutor.execute(() -> { - PinnedStackListenerForwarder.this.onActivityHidden(componentName); - }); - } } /** @@ -108,7 +94,5 @@ public class PinnedStackListenerForwarder { public void onMovementBoundsChanged(boolean fromImeAdjustment) {} public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {} - - public void onActivityHidden(ComponentName componentName) {} } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java index a749019046f8..b27c428f1693 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java @@ -16,10 +16,12 @@ package com.android.wm.shell.pip; +import android.annotation.NonNull; import android.graphics.Rect; import com.android.wm.shell.shared.annotations.ExternalThread; +import java.util.concurrent.Executor; import java.util.function.Consumer; /** @@ -69,9 +71,10 @@ public interface Pip { default void removePipExclusionBoundsChangeListener(Consumer<Rect> listener) { } /** - * @return {@link PipTransitionController} instance. + * Register {@link PipTransitionController.PipTransitionCallback} to listen on PiP transition + * started / finished callbacks. */ - default PipTransitionController getPipTransitionController() { - return null; - } + default void registerPipTransitionCallback( + @NonNull PipTransitionController.PipTransitionCallback callback, + @NonNull Executor executor) { } } 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 0a3c15b6057f..f060158766fe 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 @@ -17,6 +17,7 @@ package com.android.wm.shell.pip; import static android.util.RotationUtils.rotateBounds; +import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; @@ -37,11 +38,12 @@ import android.window.TaskSnapshot; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.IconProvider; -import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.animation.Interpolators; +import com.android.wm.shell.shared.pip.PipContentOverlay; import com.android.wm.shell.transition.Transitions; import java.lang.annotation.Retention; @@ -229,6 +231,7 @@ public class PipAnimationController { /** * Quietly cancel the animator by removing the listeners first. + * TODO(b/275003573): deprecate this, cancelling without the proper callbacks is problematic. */ static void quietCancel(@NonNull ValueAnimator animator) { animator.removeAllUpdateListeners(); @@ -416,7 +419,7 @@ public class PipAnimationController { } SurfaceControl getContentOverlayLeash() { - return mContentOverlay == null ? null : mContentOverlay.mLeash; + return mContentOverlay == null ? null : mContentOverlay.getLeash(); } void setColorContentOverlay(Context context) { @@ -494,13 +497,8 @@ public class PipAnimationController { mCurrentValue = value; } - boolean shouldApplyCornerRadius() { - return !isOutPipDirection(mTransitionDirection); - } - boolean shouldApplyShadowRadius() { - return !isOutPipDirection(mTransitionDirection) - && !isRemovePipDirection(mTransitionDirection); + return !isRemovePipDirection(mTransitionDirection); } boolean inScaleTransition() { @@ -553,7 +551,7 @@ public class PipAnimationController { final float alpha = getStartValue() * (1 - fraction) + getEndValue() * fraction; setCurrentValue(alpha); getSurfaceTransactionHelper().alpha(tx, leash, alpha) - .round(tx, leash, shouldApplyCornerRadius()) + .round(tx, leash, true /* applyCornerRadius */) .shadow(tx, leash, shouldApplyShadowRadius()); if (!handlePipTransaction(leash, tx, destinationBounds, alpha)) { tx.apply(); @@ -569,7 +567,7 @@ public class PipAnimationController { getSurfaceTransactionHelper() .resetScale(tx, leash, getDestinationBounds()) .crop(tx, leash, getDestinationBounds()) - .round(tx, leash, shouldApplyCornerRadius()) + .round(tx, leash, true /* applyCornerRadius */) .shadow(tx, leash, shouldApplyShadowRadius()); tx.show(leash); tx.apply(); @@ -625,6 +623,14 @@ public class PipAnimationController { } } else { adjustedSourceRectHint.set(sourceRectHint); + if (isInPipDirection(direction) + && rotationDelta == ROTATION_0 + && taskInfo.displayCutoutInsets != null) { + // TODO: this is to special case the issues on Foldable device + // with display cutout. This aligns with what's in SwipePipToHomeAnimator. + adjustedSourceRectHint.offset(taskInfo.displayCutoutInsets.left, + taskInfo.displayCutoutInsets.top); + } } final Rect sourceHintRectInsets = new Rect(); if (!adjustedSourceRectHint.isEmpty()) { @@ -675,13 +681,11 @@ public class PipAnimationController { getSurfaceTransactionHelper().scaleAndCrop(tx, leash, adjustedSourceRectHint, initialSourceValue, bounds, insets, isInPipDirection, fraction); - if (shouldApplyCornerRadius()) { - final Rect sourceBounds = new Rect(initialContainerRect); - sourceBounds.inset(insets); - getSurfaceTransactionHelper() - .round(tx, leash, sourceBounds, bounds) - .shadow(tx, leash, shouldApplyShadowRadius()); - } + final Rect sourceBounds = new Rect(initialContainerRect); + sourceBounds.inset(insets); + getSurfaceTransactionHelper() + .round(tx, leash, sourceBounds, bounds) + .shadow(tx, leash, shouldApplyShadowRadius()); } if (!handlePipTransaction(leash, tx, bounds, /* alpha= */ 1f)) { tx.apply(); @@ -730,11 +734,9 @@ public class PipAnimationController { .rotateAndScaleWithCrop(tx, leash, initialContainerRect, bounds, insets, degree, x, y, isOutPipDirection, rotationDelta == ROTATION_270 /* clockwise */); - if (shouldApplyCornerRadius()) { - getSurfaceTransactionHelper() - .round(tx, leash, sourceBounds, bounds) - .shadow(tx, leash, shouldApplyShadowRadius()); - } + getSurfaceTransactionHelper() + .round(tx, leash, sourceBounds, bounds) + .shadow(tx, leash, shouldApplyShadowRadius()); if (!handlePipTransaction(leash, tx, bounds, 1f /* alpha */)) { tx.apply(); } @@ -750,7 +752,7 @@ public class PipAnimationController { void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) { getSurfaceTransactionHelper() .alpha(tx, leash, 1f) - .round(tx, leash, shouldApplyCornerRadius()) + .round(tx, leash, true /* applyCornerRadius */) .shadow(tx, leash, shouldApplyShadowRadius()); tx.show(leash); tx.apply(); 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 202f60dad842..3d1994cac534 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 @@ -137,7 +137,7 @@ public class PipSurfaceTransactionHelper { mTmpDestinationRect.inset(insets); // Scale to the bounds no smaller than the destination and offset such that the top/left // of the scaled inset source rect aligns with the top/left of the destination bounds - final float scale, left, top; + final float scale; if (isInPipDirection && sourceRectHint != null && sourceRectHint.width() < sourceBounds.width()) { // scale by sourceRectHint if it's not edge-to-edge, for entering PiP transition only. @@ -148,14 +148,17 @@ public class PipSurfaceTransactionHelper { ? (float) destinationBounds.width() / sourceBounds.width() : (float) destinationBounds.height() / sourceBounds.height(); scale = (1 - fraction) * startScale + fraction * endScale; - left = destinationBounds.left - insets.left * scale; - top = destinationBounds.top - insets.top * scale; } else { scale = Math.max((float) destinationBounds.width() / sourceBounds.width(), (float) destinationBounds.height() / sourceBounds.height()); - // Work around the rounding error by fix the position at very beginning. - left = scale == 1 ? 0 : destinationBounds.left - insets.left * scale; - top = scale == 1 ? 0 : destinationBounds.top - insets.top * scale; + } + float left = destinationBounds.left - insets.left * scale; + float top = destinationBounds.top - insets.top * scale; + if (scale == 1) { + // Work around the 1 pixel off error by rounding the position down at very beginning. + // We noticed such error from flicker tests, not visually. + left = sourceBounds.left; + top = sourceBounds.top; } mTmpTransform.setScale(scale, scale); tx.setMatrix(leash, mTmpTransform, mTmpFloat9) 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 7004b713cff7..b4e03299f14c 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 @@ -25,8 +25,6 @@ import static android.util.RotationUtils.rotateBounds; import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_PIP; import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_BOUNDS; import static com.android.wm.shell.pip.PipAnimationController.FRACTION_START; @@ -42,6 +40,8 @@ import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTI import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection; import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; import static com.android.wm.shell.pip.PipAnimationController.isRemovePipDirection; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; @@ -73,10 +73,9 @@ import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.ShellExecutor; @@ -90,7 +89,9 @@ import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.pip.phone.PipMotionHelper; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.animation.Interpolators; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.pip.PipContentOverlay; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.transition.Transitions; @@ -98,6 +99,7 @@ import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.Objects; import java.util.Optional; +import java.util.StringJoiner; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -361,8 +363,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, SurfaceControl mPipOverlay; /** - * The app bounds used for the buffer size of the - * {@link com.android.wm.shell.pip.PipContentOverlay.PipAppIconOverlay}. + * The app bounds used for the buffer size of the {@link PipContentOverlay.PipAppIconOverlay}. * * Note that this is empty if the overlay is removed or if it's some other type of overlay * defined in {@link PipContentOverlay}. @@ -423,7 +424,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, }); mPipTransitionController.setPipOrganizer(this); displayController.addDisplayWindowListener(this); - pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback); + pipTransitionController.registerPipTransitionCallback( + mPipTransitionCallback, mMainExecutor); } } @@ -495,7 +497,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "startSwipePipToHome: %s, state=%s", componentName, mPipTransitionState); mPipTransitionState.setInSwipePipToHomeTransition(true); - sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP); + if (ENABLE_SHELL_TRANSITIONS) { + mPipTransitionController.onStartSwipePipToHome(); + } else { + sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP); + } setBoundsStateForEntry(componentName, pictureInPictureParams, activityInfo); return mPipBoundsAlgorithm.getEntryDestinationBounds(); } @@ -605,6 +611,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public void exitPip(int animationDurationMs, boolean requestEnterSplit) { if (!mPipTransitionState.isInPip() || mPipTransitionState.getTransitionState() == PipTransitionState.EXITING_PIP + || mPipTransitionState.getInSwipePipToHomeTransition() || mToken == null) { ProtoLog.wtf(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Not allowed to exitPip in current state" @@ -825,6 +832,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPictureInPictureParams.getTitle()); mPipParamsChangedForwarder.notifySubtitleChanged( mPictureInPictureParams.getSubtitle()); + logRemoteActions(mPictureInPictureParams); } mPipUiEventLoggerLogger.setTaskInfo(mTaskInfo); @@ -1106,6 +1114,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } applyNewPictureInPictureParams(newParams); mPictureInPictureParams = newParams; + logRemoteActions(mPictureInPictureParams); } @Override @@ -1414,6 +1423,16 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } } + private void logRemoteActions(@NonNull PictureInPictureParams params) { + StringJoiner sj = new StringJoiner("|", "[", "]"); + if (params.hasSetActions()) { + params.getActions().forEach((action) -> sj.add(action.getTitle())); + } + + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: PIP remote actions=%s", TAG, sj.toString()); + } + /** * Animates resizing of the pinned stack given the duration. */ @@ -1979,12 +1998,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } clearContentOverlay(); } - if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { - // Avoid double removal, which is fatal. - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: trying to remove overlay (%s) while in UNDEFINED state", TAG, surface); - return; - } if (surface == null || !surface.isValid()) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: trying to remove invalid content overlay (%s)", TAG, surface); @@ -2020,7 +2033,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, tx.apply(); } - private void cancelCurrentAnimator() { + /** + * Cancels the currently running animator if there is one and removes an overlay if present. + */ + public void cancelCurrentAnimator() { final PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController.getCurrentAnimator(); // remove any overlays if present @@ -2028,7 +2044,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, removeContentOverlay(mPipOverlay, null /* callback */); } if (animator != null) { - PipAnimationController.quietCancel(animator); + animator.cancel(); mPipAnimationController.resetAnimatorState(); } } 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 3cae72d89ecc..2138acc51eb2 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 @@ -21,9 +21,11 @@ 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.util.RotationUtils.rotateBounds; +import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_BACK; @@ -39,6 +41,7 @@ import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTI import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection; import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; import static com.android.wm.shell.pip.PipTransitionState.ENTERED_PIP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_CLEANUP_PIP_EXIT; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; @@ -62,7 +65,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; @@ -72,6 +75,7 @@ import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.TransitionUtil; +import com.android.wm.shell.shared.pip.PipContentOverlay; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.CounterRotatorHelper; @@ -120,6 +124,8 @@ public class PipTransition extends PipTransitionController { @Nullable private IBinder mMoveToBackTransition; private IBinder mRequestedEnterTransition; + private IBinder mCleanupTransition; + private WindowContainerToken mRequestedEnterTask; /** The Task window that is currently in PIP windowing mode. */ @Nullable @@ -189,6 +195,11 @@ public class PipTransition extends PipTransitionController { } @Override + protected boolean isInSwipePipToHomeTransition() { + return mPipTransitionState.getInSwipePipToHomeTransition(); + } + + @Override public void startExitTransition(int type, WindowContainerTransaction out, @Nullable Rect destinationBounds) { if (destinationBounds != null) { @@ -225,10 +236,12 @@ public class PipTransition extends PipTransitionController { // Exiting PIP. final int type = info.getType(); - if (transition.equals(mExitTransition) || transition.equals(mMoveToBackTransition)) { + if (transition.equals(mExitTransition) || transition.equals(mMoveToBackTransition) + || transition.equals(mCleanupTransition)) { mExitDestinationBounds.setEmpty(); mExitTransition = null; mMoveToBackTransition = null; + mCleanupTransition = null; mHasFadeOut = false; if (mFinishCallback != null) { callFinishCallback(null /* wct */); @@ -262,6 +275,9 @@ public class PipTransition extends PipTransitionController { removePipImmediately(info, startTransaction, finishTransaction, finishCallback, pipTaskInfo); break; + case TRANSIT_CLEANUP_PIP_EXIT: + cleanupPipExitTransition(startTransaction, finishCallback); + break; default: throw new IllegalStateException("mExitTransition with unexpected transit type=" + transitTypeToString(type)); @@ -283,6 +299,20 @@ public class PipTransition extends PipTransitionController { // Entering PIP. if (isEnteringPip(info)) { + if (!mPipTransitionState.isInPip() && TransitionUtil.hasDisplayChange(info)) { + final TransitionInfo.Change pipChange = getPipChange(info); + if (pipChange != null) { + // Clear old crop. + updatePipForUnhandledTransition(pipChange, startTransaction, finishTransaction); + } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: ignore exited PiP with display change", TAG); + // This should be an exited pip. E.g. a display change transition happens when + // the exiting pip is animating, then mergeAnimation -> end -> onFinishResize -> + // onExitPipFinished was called, i.e. pip state is UNDEFINED. So do not handle + // the incoming transition as entering pip. + return false; + } if (handleEnteringPipWithDisplayChange(transition, info, startTransaction, finishTransaction, finishCallback)) { // The destination position is applied directly and let default transition handler @@ -300,6 +330,10 @@ public class PipTransition extends PipTransitionController { finishTransaction); } + if (isCurrentPipActivityClosed(info)) { + mPipBoundsState.setLastPipComponentName(null /* componentName */); + } + return false; } @@ -322,6 +356,21 @@ public class PipTransition extends PipTransitionController { return true; } + private boolean isCurrentPipActivityClosed(TransitionInfo info) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + TransitionInfo.Change change = info.getChanges().get(i); + boolean isTaskChange = change.getTaskInfo() != null; + boolean hasComponentNameOfPip = change.getActivityComponent() != null + && change.getActivityComponent().equals( + mPipBoundsState.getLastPipComponentName()); + if (!isTaskChange && change.getMode() == TRANSIT_CLOSE && hasComponentNameOfPip) { + return true; + } + } + return false; + } + + @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @@ -728,7 +777,19 @@ public class PipTransition extends PipTransitionController { mPipAnimationController.resetAnimatorState(); finishTransaction.remove(pipLeash); } - finishCallback.onTransitionFinished(wct); + + if (mFixedRotationState == FIXED_ROTATION_TRANSITION) { + // TODO(b/358226697): start a new transition with the WCT instead of applying it in + // the {@link finishCallback}, to ensure shell creates a transition for it. + finishCallback.onTransitionFinished(wct); + } else { + // Apply wct in separate transition so that it can be correctly handled by the + // {@link FreeformTaskTransitionObserver} when desktop windowing (which does not + // utilize fixed rotation transitions for exiting pip) is enabled (See b/288910069). + mCleanupTransition = mTransitions.startTransition( + TRANSIT_CLEANUP_PIP_EXIT, wct, this); + finishCallback.onTransitionFinished(null); + } }; mFinishTransaction = finishTransaction; @@ -874,6 +935,16 @@ public class PipTransition extends PipTransitionController { finishCallback.onTransitionFinished(null); } + /** + * For {@link Transitions#TRANSIT_CLEANUP_PIP_EXIT} which applies final config changes needed + * after the exit from pip transition animation finishes. + */ + private void cleanupPipExitTransition(@NonNull SurfaceControl.Transaction startTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + startTransaction.apply(); + finishCallback.onTransitionFinished(null); + } + /** Whether we should handle the given {@link TransitionInfo} animation as entering PIP. */ private boolean isEnteringPip(@NonNull TransitionInfo info) { for (int i = info.getChanges().size() - 1; i >= 0; --i) { @@ -1000,6 +1071,9 @@ public class PipTransition extends PipTransitionController { mPipMenuController.attach(leash); } + // Make sure we have the launcher shelf into destination bounds calculation + // before the animator starts. + mPipBoundsState.mayUseCachedLauncherShelfHeight(); final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); final Rect currentBounds = pipChange.getStartAbsBounds(); @@ -1028,6 +1102,8 @@ public class PipTransition extends PipTransitionController { return; } + // NOTE(b/365300020): Legacy enter PiP path, clear the swipe state. + mPipTransitionState.setInSwipePipToHomeTransition(false); final int enterAnimationType = mEnterAnimationType; if (enterAnimationType == ANIM_TYPE_ALPHA) { startTransaction.setAlpha(leash, 0f); @@ -1153,11 +1229,22 @@ public class PipTransition extends PipTransitionController { .setLayer(swipePipToHomeOverlay, Integer.MAX_VALUE); } - final Rect sourceBounds = pipTaskInfo.configuration.windowConfiguration.getBounds(); + sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP); + // Both Shell and Launcher calculate their own "adjusted" source-rect-hint values based on + // appBounds being source bounds when entering PiP. + final Rect sourceBounds = swipePipToHomeOverlay == null + ? pipTaskInfo.configuration.windowConfiguration.getBounds() + : mPipOrganizer.mAppBounds; + + // Populate the final surface control transactions from PipTransitionAnimator, + // display cutout insets is handled in the swipe pip to home animator, empty it out here + // to avoid flicker. + final Rect savedDisplayCutoutInsets = new Rect(pipTaskInfo.displayCutoutInsets); + pipTaskInfo.displayCutoutInsets.setEmpty(); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController.getAnimator(pipTaskInfo, leash, sourceBounds, sourceBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, - 0 /* startingAngle */, 0 /* rotationDelta */) + 0 /* startingAngle */, ROTATION_0 /* rotationDelta */) .setPipTransactionHandler(mTransactionConsumer) .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP); // The start state is the end state for swipe-auto-pip. @@ -1165,6 +1252,7 @@ public class PipTransition extends PipTransitionController { animator.applySurfaceControlTransaction(leash, startTransaction, PipAnimationController.FRACTION_END); startTransaction.apply(); + pipTaskInfo.displayCutoutInsets.set(savedDisplayCutoutInsets); mPipBoundsState.setBounds(destinationBounds); final SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); 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 6eefdcfc4d93..9b815817d4d3 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 @@ -41,7 +41,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; @@ -53,8 +53,9 @@ import com.android.wm.shell.transition.DefaultMixedHandler; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executor; /** * Responsible supplying PiP Transitions. @@ -66,7 +67,7 @@ public abstract class PipTransitionController implements Transitions.TransitionH protected final ShellTaskOrganizer mShellTaskOrganizer; protected final PipMenuController mPipMenuController; protected final Transitions mTransitions; - private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>(); + private final Map<PipTransitionCallback, Executor> mPipTransitionCallbacks = new HashMap<>(); protected PipTaskOrganizer mPipOrganizer; protected DefaultMixedHandler mMixedHandler; @@ -126,8 +127,10 @@ public abstract class PipTransitionController implements Transitions.TransitionH /** * Called when the Shell wants to start resizing Pip transition/animation. + * + * @param duration the suggested duration for resize animation. */ - public void startResizeTransition(WindowContainerTransaction wct) { + public void startResizeTransition(WindowContainerTransaction wct, int duration) { // Default implementation does nothing. } @@ -181,18 +184,49 @@ public abstract class PipTransitionController implements Transitions.TransitionH /** * Registers {@link PipTransitionCallback} to receive transition callbacks. */ - public void registerPipTransitionCallback(PipTransitionCallback callback) { - mPipTransitionCallbacks.add(callback); + public void registerPipTransitionCallback( + @NonNull PipTransitionCallback callback, @NonNull Executor executor) { + mPipTransitionCallbacks.put(callback, executor); + } + + protected void onStartSwipePipToHome() { + if (Flags.enablePipUiStateCallbackOnEntering()) { + try { + ActivityTaskManager.getService().onPictureInPictureUiStateChanged( + new PictureInPictureUiState.Builder() + .setTransitioningToPip(true) + .build()); + } catch (RemoteException | IllegalStateException e) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "Failed to set alert PiP state change."); + } + } + } + + /** + * Used in {@link #sendOnPipTransitionStarted(int)} to decide whether we should send the + * PictureInPictureUiState change callback on transition start. + * For instance, in auto-enter-pip case, {@link #onStartSwipePipToHome()} should have signaled + * the app, and we can return {@code true} here to avoid double callback. + * + * @return {@code true} if there is a ongoing swipe pip to home transition. + */ + protected boolean isInSwipePipToHomeTransition() { + return false; } protected void sendOnPipTransitionStarted( @PipAnimationController.TransitionDirection int direction) { final Rect pipBounds = mPipBoundsState.getBounds(); - for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { - final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); - callback.onPipTransitionStarted(direction, pipBounds); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "sendOnPipTransitionStarted direction=%d, bounds=%s", direction, pipBounds); + for (Map.Entry<PipTransitionCallback, Executor> entry + : mPipTransitionCallbacks.entrySet()) { + entry.getValue().execute( + () -> entry.getKey().onPipTransitionStarted(direction, pipBounds)); } - if (isInPipDirection(direction) && Flags.enablePipUiStateCallbackOnEntering()) { + if (isInPipDirection(direction) && Flags.enablePipUiStateCallbackOnEntering() + && !isInSwipePipToHomeTransition()) { try { ActivityTaskManager.getService().onPictureInPictureUiStateChanged( new PictureInPictureUiState.Builder() @@ -207,9 +241,12 @@ public abstract class PipTransitionController implements Transitions.TransitionH protected void sendOnPipTransitionFinished( @PipAnimationController.TransitionDirection int direction) { - for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { - final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); - callback.onPipTransitionFinished(direction); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "sendOnPipTransitionFinished direction=%d", direction); + for (Map.Entry<PipTransitionCallback, Executor> entry + : mPipTransitionCallbacks.entrySet()) { + entry.getValue().execute( + () -> entry.getKey().onPipTransitionFinished(direction)); } if (isInPipDirection(direction) && Flags.enablePipUiStateCallbackOnEntering()) { try { @@ -226,9 +263,12 @@ public abstract class PipTransitionController implements Transitions.TransitionH protected void sendOnPipTransitionCancelled( @PipAnimationController.TransitionDirection int direction) { - for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { - final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); - callback.onPipTransitionCanceled(direction); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "sendOnPipTransitionCancelled direction=%d", direction); + for (Map.Entry<PipTransitionCallback, Executor> entry + : mPipTransitionCallbacks.entrySet()) { + entry.getValue().execute( + () -> entry.getKey().onPipTransitionCanceled(direction)); } } 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 0169e8c40f45..c7369a3cd347 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 @@ -32,7 +32,7 @@ import android.view.View; import android.view.ViewRootImpl; import android.view.WindowManagerGlobal; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.pip.PipBoundsState; 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 8c4bf7620068..af6844262771 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 @@ -32,7 +32,7 @@ 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.TRANSITION_DIRECTION_USER_RESIZE; import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; -import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP; +import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_PIP; import android.app.ActivityManager; import android.app.ActivityTaskManager; @@ -44,6 +44,7 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; +import android.os.Handler; import android.os.RemoteException; import android.os.SystemProperties; import android.util.Pair; @@ -59,7 +60,7 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.common.DisplayChangeController; @@ -93,6 +94,7 @@ import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipTransitionState; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.KeyguardChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -106,6 +108,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.Executor; import java.util.function.Consumer; /** @@ -145,6 +148,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb private Optional<OneHandedController> mOneHandedController; private final ShellCommandHandler mShellCommandHandler; private final ShellController mShellController; + @ShellMainThread + private final Handler mHandler; protected final PipImpl mImpl; private final Rect mTmpInsetBounds = new Rect(); @@ -239,6 +244,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb */ private final DisplayChangeController.OnDisplayChangingListener mRotationController = ( displayId, fromRotation, toRotation, newDisplayAreaInfo, t) -> { + if (fromRotation == toRotation) { + // OnDisplayChangingListener also gets triggered upon Display size changes; + // in PiP1, those are handled separately by OnDisplaysChangedListener callbacks. + return; + } + if (mPipTransitionController.handleRotateDisplay(fromRotation, toRotation, t)) { return; } @@ -271,6 +282,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb final boolean changed = onDisplayRotationChanged(mContext, outBounds, currentBounds, mTmpInsetBounds, displayId, fromRotation, toRotation, t); if (changed) { + mMenuController.hideMenu(); // If the pip was in the offset zone earlier, adjust the new bounds to the bottom of the // movement bounds mTouchHandler.adjustBoundsForRotation(outBounds, mPipBoundsState.getBounds(), @@ -316,7 +328,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb return; } onDisplayChanged(mDisplayController.getDisplayLayout(displayId), - false /* saveRestoreSnapFraction */); + true /* saveRestoreSnapFraction */); } @Override @@ -367,22 +379,13 @@ public class PipController implements PipTransitionController.PipTransitionCallb false /* fromRotation */, fromImeAdjustment, false /* fromShelfAdjustment */, null /* windowContainerTransaction */); } - - @Override - public void onActivityHidden(ComponentName componentName) { - if (componentName.equals(mPipBoundsState.getLastPipComponentName())) { - // The activity was removed, we don't want to restore to the reentry state - // saved for this component anymore. - mPipBoundsState.setLastPipComponentName(null); - } - } } /** * Instantiates {@link PipController}, returns {@code null} if the feature not supported. */ @Nullable - public static Pip create(Context context, + public static PipImpl create(Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, ShellController shellController, @@ -406,7 +409,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb DisplayInsetsController displayInsetsController, TabletopModeController pipTabletopController, Optional<OneHandedController> oneHandedController, - ShellExecutor mainExecutor) { + ShellExecutor mainExecutor, + Handler handler) { if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Device doesn't support Pip feature", TAG); @@ -419,7 +423,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb pipDisplayLayoutState, pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController, windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, - displayInsetsController, pipTabletopController, oneHandedController, mainExecutor) + displayInsetsController, pipTabletopController, oneHandedController, mainExecutor, + handler) .mImpl; } @@ -447,11 +452,13 @@ public class PipController implements PipTransitionController.PipTransitionCallb DisplayInsetsController displayInsetsController, TabletopModeController tabletopModeController, Optional<OneHandedController> oneHandedController, - ShellExecutor mainExecutor + ShellExecutor mainExecutor, + @ShellMainThread Handler handler ) { mContext = context; mShellCommandHandler = shellCommandHandler; mShellController = shellController; + mHandler = handler; mImpl = new PipImpl(); mWindowManagerShellWrapper = windowManagerShellWrapper; mDisplayController = displayController; @@ -487,7 +494,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb mShellCommandHandler.addDumpCallback(this::dump, this); mPipInputConsumer = new PipInputConsumer(WindowManagerGlobal.getWindowManagerService(), INPUT_CONSUMER_PIP, mMainExecutor); - mPipTransitionController.registerPipTransitionCallback(this); + mPipTransitionController.registerPipTransitionCallback(this, mMainExecutor); mPipTaskOrganizer.registerOnDisplayIdChangeCallback((int displayId) -> { mPipDisplayLayoutState.setDisplayId(displayId); onDisplayChanged(mDisplayController.getDisplayLayout(displayId), @@ -639,6 +646,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb public void insetsChanged(InsetsState insetsState) { DisplayLayout pendingLayout = mDisplayController .getDisplayLayout(mPipDisplayLayoutState.getDisplayId()); + if (pendingLayout == null) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "insetsChanged: no display layout for displayId=%d", + mPipDisplayLayoutState.getDisplayId()); + return; + } if (mIsInFixedRotation || mIsKeyguardShowingOrAnimating || pendingLayout.rotation() @@ -647,16 +660,18 @@ public class PipController implements PipTransitionController.PipTransitionCallb // there's a keyguard present return; } - onDisplayChangedUncheck(mDisplayController - .getDisplayLayout(mPipDisplayLayoutState.getDisplayId()), - false /* saveRestoreSnapFraction */); + mMainExecutor.executeDelayed(() -> { + onDisplayChangedUncheck(mDisplayController.getDisplayLayout( + mPipDisplayLayoutState.getDisplayId()), + false /* saveRestoreSnapFraction */); + }, PIP_KEEP_CLEAR_AREAS_DELAY); } }); mTabletopModeController.registerOnTabletopModeChangedListener((isInTabletopMode) -> { - final String tag = "tabletop-mode"; if (!isInTabletopMode) { - mPipBoundsState.removeNamedUnrestrictedKeepClearArea(tag); + mPipBoundsState.setNamedUnrestrictedKeepClearArea( + PipBoundsState.NAMED_KCA_TABLETOP_MODE, null); return; } @@ -665,14 +680,16 @@ public class PipController implements PipTransitionController.PipTransitionCallb if (mTabletopModeController.getPreferredHalfInTabletopMode() == TabletopModeController.PREFERRED_TABLETOP_HALF_TOP) { // Prefer top, avoid the bottom half of the display. - mPipBoundsState.addNamedUnrestrictedKeepClearArea(tag, new Rect( - displayBounds.left, displayBounds.centerY(), - displayBounds.right, displayBounds.bottom)); + mPipBoundsState.setNamedUnrestrictedKeepClearArea( + PipBoundsState.NAMED_KCA_TABLETOP_MODE, new Rect( + displayBounds.left, displayBounds.centerY(), + displayBounds.right, displayBounds.bottom)); } else { // Prefer bottom, avoid the top half of the display. - mPipBoundsState.addNamedUnrestrictedKeepClearArea(tag, new Rect( - displayBounds.left, displayBounds.top, - displayBounds.right, displayBounds.centerY())); + mPipBoundsState.setNamedUnrestrictedKeepClearArea( + PipBoundsState.NAMED_KCA_TABLETOP_MODE, new Rect( + displayBounds.left, displayBounds.top, + displayBounds.right, displayBounds.centerY())); } // Try to move the PiP window if we have entered PiP mode. @@ -793,7 +810,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb } }; - if (mPipTaskOrganizer.isInPip() && saveRestoreSnapFraction) { + if (mPipTransitionState.hasEnteredPip() && saveRestoreSnapFraction) { mMenuController.attachPipMenuView(); // Calculate the snap fraction of the current stack along the old movement bounds final PipSnapAlgorithm pipSnapAlgorithm = mPipBoundsAlgorithm.getSnapAlgorithm(); @@ -924,10 +941,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb 0, mPipBoundsState.getDisplayBounds().bottom - height, mPipBoundsState.getDisplayBounds().right, mPipBoundsState.getDisplayBounds().bottom); - mPipBoundsState.addNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG, rect); + mPipBoundsState.setNamedUnrestrictedKeepClearArea( + PipBoundsState.NAMED_KCA_LAUNCHER_SHELF, rect); updatePipPositionForKeepClearAreas(); } else { - mPipBoundsState.removeNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG); + mPipBoundsState.setNamedUnrestrictedKeepClearArea( + PipBoundsState.NAMED_KCA_LAUNCHER_SHELF, null); // postpone moving in response to hide of Launcher in case there's another change mMainExecutor.removeCallbacks(mMovePipInResponseToKeepClearAreasChangeCallback); mMainExecutor.executeDelayed( @@ -976,8 +995,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb int launcherRotation, Rect hotseatKeepClearArea) { // preemptively add the keep clear area for Hotseat, so that it is taken into account // when calculating the entry destination bounds of PiP window - mPipBoundsState.addNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG, - hotseatKeepClearArea); + mPipBoundsState.setNamedUnrestrictedKeepClearArea( + PipBoundsState.NAMED_KCA_LAUNCHER_SHELF, hotseatKeepClearArea); onDisplayRotationChangedNotInPip(mContext, launcherRotation); // cache current min/max size Point minSize = mPipBoundsState.getMinSize(); @@ -1036,7 +1055,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb // Begin InteractionJankMonitor with PIP transition CUJs final InteractionJankMonitor.Configuration.Builder builder = InteractionJankMonitor.Configuration.Builder.withSurface( - CUJ_PIP_TRANSITION, mContext, mPipTaskOrganizer.getSurfaceControl()) + CUJ_PIP_TRANSITION, mContext, mPipTaskOrganizer.getSurfaceControl(), + mHandler) .setTag(getTransitionTag(direction)) .setTimeout(2000); InteractionJankMonitor.getInstance().begin(builder); @@ -1185,7 +1205,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb /** * The interface for calls from outside the Shell, within the host process. */ - private class PipImpl implements Pip { + public class PipImpl implements Pip { @Override public void expandPip() { mMainExecutor.execute(() -> { @@ -1229,8 +1249,11 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public PipTransitionController getPipTransitionController() { - return mPipTransitionController; + public void registerPipTransitionCallback( + PipTransitionController.PipTransitionCallback callback, + Executor executor) { + mMainExecutor.execute(() -> mPipTransitionController.registerPipTransitionCallback( + callback, executor)); } } 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 f92938989637..06d231144d81 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 @@ -35,10 +35,10 @@ import androidx.annotation.NonNull; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.DismissViewUtils; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.bubbles.DismissCircleView; -import com.android.wm.shell.common.bubbles.DismissView; -import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import com.android.wm.shell.common.pip.PipUiEventLogger; +import com.android.wm.shell.shared.bubbles.DismissCircleView; +import com.android.wm.shell.shared.bubbles.DismissView; +import com.android.wm.shell.shared.magnetictarget.MagnetizedObject; import kotlin.Unit; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java index f6cab485fa2a..d1978c30eecb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java @@ -28,7 +28,7 @@ import android.view.IWindowManager; import android.view.InputChannel; import android.view.InputEvent; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.protolog.ShellProtoLogGroup; 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 15342be0e8b7..c8b52c6b00c4 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 @@ -34,11 +34,13 @@ import android.animation.ValueAnimator; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityManager; import android.app.PendingIntent; import android.app.RemoteAction; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -59,13 +61,13 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import android.widget.LinearLayout; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; -import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.animation.Interpolators; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -151,6 +153,10 @@ public class PipMenuView extends FrameLayout { // How long the shell will wait for the app to close the PiP if a custom action is set. private final int mPipForceCloseDelay; + // Context for the currently active user. This may differ from the regular systemui Context + // in cases such as secondary users or HSUM. + private Context mContextForUser; + public PipMenuView(Context context, PhonePipMenuController controller, ShellExecutor mainExecutor, Handler mainHandler, PipUiEventLogger pipUiEventLogger) { @@ -202,6 +208,7 @@ public class PipMenuView extends FrameLayout { .getInteger(R.integer.config_pipExitAnimationDuration); initAccessibility(); + setContextForUser(); } private void initAccessibility() { @@ -476,7 +483,7 @@ public class PipMenuView extends FrameLayout { actionView.setImageDrawable(null); } else { // TODO: Check if the action drawable has changed before we reload it - action.getIcon().loadDrawableAsync(mContext, d -> { + action.getIcon().loadDrawableAsync(mContextForUser, d -> { if (d != null) { d.setTint(Color.WHITE); actionView.setImageDrawable(d); @@ -510,6 +517,33 @@ public class PipMenuView extends FrameLayout { expandContainer.requestLayout(); } + /** + * Sets the Context for the current user. If the user is the same as systemui, then simply + * use systemui Context. + */ + private void setContextForUser() { + int userId = ActivityManager.getCurrentUser(); + + if (mContext.getUserId() != userId) { + try { + mContextForUser = mContext.createPackageContextAsUser(mContext.getPackageName(), + Context.CONTEXT_RESTRICTED, new UserHandle(userId)); + } catch (PackageManager.NameNotFoundException e) { + // Shouldn't happen, use systemui context as backup + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to get context for user. Sysui userid=%d," + + " current userid=%d, error=%s", + TAG, + mContext.getUserId(), + userId, + e); + mContextForUser = mContext; + } + } else { + mContextForUser = mContext; + } + } + private void notifyMenuStateChangeStart(int menuState, boolean resize, Runnable callback) { mController.onMenuStateChangeStart(menuState, resize, callback); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java index ef468434db6a..5710af645a73 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -34,11 +34,11 @@ import android.graphics.PointF; import android.graphics.Rect; import android.os.Debug; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.animation.FloatProperties; import com.android.wm.shell.common.FloatingContentCoordinator; -import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipAppOpsListener; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipPerfHintController; @@ -47,6 +47,8 @@ import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.animation.PhysicsAnimator; +import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.magnetictarget.MagnetizedObject; import kotlin.Unit; import kotlin.jvm.functions.Function0; @@ -171,7 +173,9 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, public void onPipTransitionCanceled(int direction) {} }; - public PipMotionHelper(Context context, @NonNull PipBoundsState pipBoundsState, + public PipMotionHelper(Context context, + @ShellMainThread ShellExecutor mainExecutor, + @NonNull PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer, PhonePipMenuController menuController, PipSnapAlgorithm snapAlgorithm, PipTransitionController pipTransitionController, FloatingContentCoordinator floatingContentCoordinator, @@ -183,7 +187,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, mSnapAlgorithm = snapAlgorithm; mFloatingContentCoordinator = floatingContentCoordinator; mPipPerfHintController = pipPerfHintControllerOptional.orElse(null); - pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback); + pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback, mainExecutor); mResizePipUpdateListener = (target, values) -> { if (mPipBoundsState.getMotionBoundsState().isInMotion()) { mPipTaskOrganizer.scheduleUserResizePip(getBounds(), @@ -200,8 +204,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, @NonNull @Override public Rect getFloatingBoundsOnScreen() { - return !mPipBoundsState.getMotionBoundsState().getAnimatingToBounds().isEmpty() - ? mPipBoundsState.getMotionBoundsState().getAnimatingToBounds() : getBounds(); + return getBounds(); } @NonNull @@ -412,6 +415,17 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, // location now. mSpringingToTouch = false; + // Boost the velocityX if it's zero to forcefully push it towards the nearest edge. + // We don't simply change the xEndValue below since the PhysicsAnimator would rely on the + // same velocityX to find out which edge to snap to. + if (velocityX == 0) { + final int motionCenterX = mPipBoundsState + .getMotionBoundsState().getBoundsInMotion().centerX(); + final int displayCenterX = mPipBoundsState + .getDisplayBounds().centerX(); + velocityX = (motionCenterX < displayCenterX) ? -0.001f : 0.001f; + } + mTemporaryBoundsPhysicsAnimator .spring(FloatProperties.RECT_WIDTH, getBounds().width(), mSpringConfig) .spring(FloatProperties.RECT_HEIGHT, getBounds().height(), mSpringConfig) @@ -601,7 +615,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, cancelPhysicsAnimation(); } - setAnimatingToBounds(new Rect( + mPipBoundsState.getMotionBoundsState().setAnimatingToBounds(new Rect( (int) toX, (int) toY, (int) toX + getBounds().width(), @@ -645,6 +659,9 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, // All motion operations have actually finished. mPipBoundsState.setBounds( mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); + // Notifies the floating coordinator that we moved, so we return these bounds from + // {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}. + mFloatingContentCoordinator.onContentMoved(this); mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded(); if (!mDismissalPending) { // do not schedule resize if PiP is dismissing, which may cause app re-open to @@ -659,16 +676,6 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, } /** - * Notifies the floating coordinator that we're moving, and sets the animating to bounds so - * we return these bounds from - * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}. - */ - private void setAnimatingToBounds(Rect bounds) { - mPipBoundsState.getMotionBoundsState().setAnimatingToBounds(bounds); - mFloatingContentCoordinator.onContentMoved(this); - } - - /** * Directly resizes the PiP to the given {@param bounds}. */ private void resizePipUnchecked(Rect toBounds) { @@ -697,7 +704,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, // This is so all the proper callbacks are performed. mPipTaskOrganizer.scheduleAnimateResizePip(toBounds, duration, TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND, null /* updateBoundsCallback */); - setAnimatingToBounds(toBounds); + mPipBoundsState.getMotionBoundsState().setAnimatingToBounds(toBounds); } /** 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 d8ac8e948a97..9c4e723efc23 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 @@ -48,7 +48,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java index 5d858fa9aa3f..cb82db630715 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchState.java @@ -23,7 +23,7 @@ import android.view.VelocityTracker; import android.view.ViewConfiguration; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.protolog.ShellProtoLogGroup; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java index 6b890c49b713..50d22ad00b11 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java @@ -33,7 +33,7 @@ import android.app.RemoteAction; import android.content.Context; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.pip.PipMediaController; import com.android.wm.shell.common.pip.PipUtils; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBackgroundView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBackgroundView.java index 0221db836dda..eb7a10cc9dfb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBackgroundView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBackgroundView.java @@ -28,7 +28,7 @@ import android.widget.FrameLayout; import androidx.annotation.NonNull; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.protolog.ShellProtoLogGroup; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java index 72c0cd71f198..188c35ff3562 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java @@ -33,7 +33,7 @@ import android.view.Gravity; import androidx.annotation.NonNull; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java index 8a215b4b2e25..1afb470c5e9b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java @@ -26,7 +26,7 @@ import android.graphics.Rect; import android.os.Handler; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement; import com.android.wm.shell.protolog.ShellProtoLogGroup; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java index 3d286461ef79..0ed5079b7fba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java @@ -39,7 +39,7 @@ import android.view.Gravity; import androidx.annotation.NonNull; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.common.DisplayController; @@ -257,7 +257,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } private void onInit() { - mPipTransitionController.registerPipTransitionCallback(this); + mPipTransitionController.registerPipTransitionCallback(this, mMainExecutor); reloadResources(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java index 977aad4a898a..327ceef00e6a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java @@ -27,7 +27,7 @@ import android.content.Context; import android.os.Bundle; import android.os.Handler; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.TvWindowMenuActionButton; import com.android.wm.shell.protolog.ShellProtoLogGroup; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index 6b5bdd2299e1..e74870d4d139 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -37,7 +37,7 @@ import android.window.SurfaceSyncGroup; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.pip.PipMenuController; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java index adc03cf5c4d7..eabf1b0b3063 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java @@ -39,7 +39,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import java.util.Arrays; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java index 4a767ef2a113..c7704f0b4eed 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -50,7 +50,7 @@ import android.widget.ImageView; import androidx.annotation.NonNull; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.internal.widget.LinearLayoutManager; import com.android.internal.widget.RecyclerView; import com.android.wm.shell.R; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java index 54e162bba2f3..ce5079227b61 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java @@ -33,7 +33,7 @@ import android.os.Bundle; import android.text.TextUtils; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.internal.util.ImageUtils; import com.android.wm.shell.R; import com.android.wm.shell.common.pip.PipMediaController; 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 ca0d61f8fc9b..7a0e6694cb51 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 @@ -62,7 +62,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.pip.PipDisplayLayoutState; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java new file mode 100644 index 000000000000..8ebdc96c21a3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2024 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.pip2.animation; + +import android.animation.Animator; +import android.animation.RectEvaluator; +import android.animation.ValueAnimator; +import android.annotation.IntDef; +import android.content.Context; +import android.graphics.Rect; +import android.view.Surface; +import android.view.SurfaceControl; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.R; +import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Animator that handles bounds animations for entering / exiting PIP. + */ +public class PipEnterExitAnimator extends ValueAnimator + implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener { + @IntDef(prefix = {"BOUNDS_"}, value = { + BOUNDS_ENTER, + BOUNDS_EXIT + }) + + @Retention(RetentionPolicy.SOURCE) + public @interface BOUNDS {} + + public static final int BOUNDS_ENTER = 0; + public static final int BOUNDS_EXIT = 1; + + @NonNull private final SurfaceControl mLeash; + private final SurfaceControl.Transaction mStartTransaction; + private final SurfaceControl.Transaction mFinishTransaction; + private final int mEnterExitAnimationDuration; + private final @BOUNDS int mDirection; + private final @Surface.Rotation int mRotation; + + // optional callbacks for tracking animation start and end + @Nullable private Runnable mAnimationStartCallback; + @Nullable private Runnable mAnimationEndCallback; + + private final Rect mBaseBounds = new Rect(); + private final Rect mStartBounds = new Rect(); + private final Rect mEndBounds = new Rect(); + + @Nullable private final Rect mSourceRectHint; + private final Rect mSourceRectHintInsets = new Rect(); + private final Rect mZeroInsets = new Rect(0, 0, 0, 0); + + // Bounds updated by the evaluator as animator is running. + private final Rect mAnimatedRect = new Rect(); + + private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + mSurfaceControlTransactionFactory; + private final RectEvaluator mRectEvaluator; + private final RectEvaluator mInsetEvaluator; + private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper; + + public PipEnterExitAnimator(Context context, + @NonNull SurfaceControl leash, + SurfaceControl.Transaction startTransaction, + SurfaceControl.Transaction finishTransaction, + @NonNull Rect baseBounds, + @NonNull Rect startBounds, + @NonNull Rect endBounds, + @Nullable Rect sourceRectHint, + @BOUNDS int direction, + @Surface.Rotation int rotation) { + mLeash = leash; + mStartTransaction = startTransaction; + mFinishTransaction = finishTransaction; + mBaseBounds.set(baseBounds); + mStartBounds.set(startBounds); + mAnimatedRect.set(startBounds); + mEndBounds.set(endBounds); + mRectEvaluator = new RectEvaluator(mAnimatedRect); + mInsetEvaluator = new RectEvaluator(new Rect()); + mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(context); + mDirection = direction; + mRotation = rotation; + + mSourceRectHint = sourceRectHint != null ? new Rect(sourceRectHint) : null; + if (mSourceRectHint != null) { + mSourceRectHintInsets.set( + mSourceRectHint.left - mBaseBounds.left, + mSourceRectHint.top - mBaseBounds.top, + mBaseBounds.right - mSourceRectHint.right, + mBaseBounds.bottom - mSourceRectHint.bottom + ); + } + + mSurfaceControlTransactionFactory = + new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); + mEnterExitAnimationDuration = context.getResources() + .getInteger(R.integer.config_pipEnterAnimationDuration); + + setObjectValues(startBounds, endBounds); + setDuration(mEnterExitAnimationDuration); + setEvaluator(mRectEvaluator); + addListener(this); + addUpdateListener(this); + } + + public void setAnimationStartCallback(@NonNull Runnable runnable) { + mAnimationStartCallback = runnable; + } + + public void setAnimationEndCallback(@NonNull Runnable runnable) { + mAnimationEndCallback = runnable; + } + + @Override + public void onAnimationStart(@NonNull Animator animation) { + if (mAnimationStartCallback != null) { + mAnimationStartCallback.run(); + } + if (mStartTransaction != null) { + mStartTransaction.apply(); + } + } + + @Override + public void onAnimationEnd(@NonNull Animator animation) { + if (mFinishTransaction != null) { + // finishTransaction might override some state (eg. corner radii) so we want to + // manually set the state to the end of the animation + mPipSurfaceTransactionHelper.scaleAndCrop(mFinishTransaction, mLeash, mSourceRectHint, + mBaseBounds, mAnimatedRect, getInsets(1f), isInPipDirection(), 1f) + .round(mFinishTransaction, mLeash, isInPipDirection()) + .shadow(mFinishTransaction, mLeash, isInPipDirection()); + } + if (mAnimationEndCallback != null) { + mAnimationEndCallback.run(); + } + } + + @Override + public void onAnimationUpdate(@NonNull ValueAnimator animation) { + final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); + final float fraction = getAnimatedFraction(); + Rect insets = getInsets(fraction); + + // TODO (b/350801661): implement fixed rotation + + mPipSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mSourceRectHint, + mBaseBounds, mAnimatedRect, insets, isInPipDirection(), fraction) + .round(tx, mLeash, isInPipDirection()) + .shadow(tx, mLeash, isInPipDirection()); + tx.apply(); + } + + private Rect getInsets(float fraction) { + Rect startInsets = isInPipDirection() ? mZeroInsets : mSourceRectHintInsets; + Rect endInsets = isInPipDirection() ? mSourceRectHintInsets : mZeroInsets; + + return mInsetEvaluator.evaluate(fraction, startInsets, endInsets); + } + + private boolean isInPipDirection() { + return mDirection == BOUNDS_ENTER; + } + + private boolean isOutPipDirection() { + return mDirection == BOUNDS_EXIT; + } + + // no-ops + + @Override + public void onAnimationCancel(@NonNull Animator animation) {} + + @Override + public void onAnimationRepeat(@NonNull Animator animation) {} +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java index 5c561fed89c7..d565776c9917 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java @@ -47,9 +47,17 @@ public class PipResizeAnimator extends ValueAnimator @Nullable private Runnable mAnimationEndCallback; private RectEvaluator mRectEvaluator; + + // Bounds relative to which scaling/cropping must be done. private final Rect mBaseBounds = new Rect(); + + // Bounds to animate from. private final Rect mStartBounds = new Rect(); + + // Target bounds. private final Rect mEndBounds = new Rect(); + + // Bounds updated by the evaluator as animator is running. private final Rect mAnimatedRect = new Rect(); private final float mDelta; @@ -84,7 +92,6 @@ public class PipResizeAnimator extends ValueAnimator addListener(this); addUpdateListener(this); setEvaluator(mRectEvaluator); - // TODO: change this setDuration(duration); } @@ -127,9 +134,10 @@ public class PipResizeAnimator extends ValueAnimator Rect baseBounds, Rect targetBounds, float degrees) { Matrix transformTensor = new Matrix(); final float[] mMatrixTmp = new float[9]; - final float scale = (float) targetBounds.width() / baseBounds.width(); + final float scaleX = (float) targetBounds.width() / baseBounds.width(); + final float scaleY = (float) targetBounds.height() / baseBounds.height(); - transformTensor.setScale(scale, scale); + transformTensor.setScale(scaleX, scaleY); transformTensor.postTranslate(targetBounds.left, targetBounds.top); transformTensor.postRotate(degrees, targetBounds.centerX(), targetBounds.centerY()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java index 6e36a32ac931..9cfe1620a2ff 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java @@ -32,7 +32,7 @@ import android.view.View; import android.view.ViewRootImpl; import android.view.WindowManagerGlobal; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.pip.PipBoundsState; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java index fc0d36d13b2e..e9c4c14234e6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java @@ -19,7 +19,7 @@ package com.android.wm.shell.pip2.phone; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; -import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP; +import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_PIP; import android.app.ActivityManager; import android.app.PictureInPictureParams; @@ -29,21 +29,24 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; -import android.view.InsetsState; import android.view.SurfaceControl; +import android.window.DisplayAreaInfo; +import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.internal.util.Preconditions; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayChangeController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ExternalInterfaceBinder; +import com.android.wm.shell.common.ImeListener; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; @@ -55,6 +58,7 @@ import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipUtils; +import com.android.wm.shell.pip.Pip; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -62,13 +66,15 @@ import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import java.io.PrintWriter; +import java.util.function.Consumer; /** * Manages the picture-in-picture (PIP) UI and states for Phones. */ public class PipController implements ConfigurationChangeListener, PipTransitionState.PipTransitionStateChangedListener, - DisplayController.OnDisplaysChangedListener, RemoteCallable<PipController> { + DisplayController.OnDisplaysChangedListener, + DisplayChangeController.OnDisplayChangingListener, RemoteCallable<PipController> { private static final String TAG = PipController.class.getSimpleName(); private static final String SWIPE_TO_PIP_APP_BOUNDS = "pip_app_bounds"; private static final String SWIPE_TO_PIP_OVERLAY = "swipe_to_pip_overlay"; @@ -85,7 +91,10 @@ public class PipController implements ConfigurationChangeListener, private final TaskStackListenerImpl mTaskStackListener; private final ShellTaskOrganizer mShellTaskOrganizer; private final PipTransitionState mPipTransitionState; + private final PipTouchHandler mPipTouchHandler; private final ShellExecutor mMainExecutor; + private final PipImpl mImpl; + private Consumer<Boolean> mOnIsInPipStateChangedListener; // Wrapper for making Binder calls into PiP animation listener hosted in launcher's Recents. private PipAnimationListener mPipRecentsAnimationListener; @@ -125,6 +134,7 @@ public class PipController implements ConfigurationChangeListener, TaskStackListenerImpl taskStackListener, ShellTaskOrganizer shellTaskOrganizer, PipTransitionState pipTransitionState, + PipTouchHandler pipTouchHandler, ShellExecutor mainExecutor) { mContext = context; mShellCommandHandler = shellCommandHandler; @@ -139,7 +149,9 @@ public class PipController implements ConfigurationChangeListener, mShellTaskOrganizer = shellTaskOrganizer; mPipTransitionState = pipTransitionState; mPipTransitionState.addPipTransitionStateChangedListener(this); + mPipTouchHandler = pipTouchHandler; mMainExecutor = mainExecutor; + mImpl = new PipImpl(); if (PipUtils.isPip2ExperimentEnabled()) { shellInit.addInitCallback(this::onInit, this); @@ -162,6 +174,7 @@ public class PipController implements ConfigurationChangeListener, TaskStackListenerImpl taskStackListener, ShellTaskOrganizer shellTaskOrganizer, PipTransitionState pipTransitionState, + PipTouchHandler pipTouchHandler, ShellExecutor mainExecutor) { if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, @@ -171,7 +184,11 @@ public class PipController implements ConfigurationChangeListener, return new PipController(context, shellInit, shellCommandHandler, shellController, displayController, displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer, - pipTransitionState, mainExecutor); + pipTransitionState, pipTouchHandler, mainExecutor); + } + + public PipImpl getPipImpl() { + return mImpl; } private void onInit() { @@ -182,13 +199,12 @@ public class PipController implements ConfigurationChangeListener, DisplayLayout layout = new DisplayLayout(mContext, mContext.getDisplay()); mPipDisplayLayoutState.setDisplayLayout(layout); - mDisplayController.addDisplayWindowListener(this); + mDisplayController.addDisplayChangingController(this); mDisplayInsetsController.addInsetsChangedListener(mPipDisplayLayoutState.getDisplayId(), - new DisplayInsetsController.OnInsetsChangedListener() { + new ImeListener(mDisplayController, mPipDisplayLayoutState.getDisplayId()) { @Override - public void insetsChanged(InsetsState insetsState) { - onDisplayChanged(mDisplayController - .getDisplayLayout(mPipDisplayLayoutState.getDisplayId())); + public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { + mPipTouchHandler.onImeVisibilityChanged(imeVisible, imeHeight); } }); @@ -243,11 +259,12 @@ public class PipController implements ConfigurationChangeListener, @Override public void onThemeChanged() { - onDisplayChanged(new DisplayLayout(mContext, mContext.getDisplay())); + setDisplayLayout(new DisplayLayout(mContext, mContext.getDisplay())); } // - // DisplayController.OnDisplaysChangedListener implementations + // DisplayController.OnDisplaysChangedListener and + // DisplayChangeController.OnDisplayChangingListener implementations // @Override @@ -255,18 +272,46 @@ public class PipController implements ConfigurationChangeListener, if (displayId != mPipDisplayLayoutState.getDisplayId()) { return; } - onDisplayChanged(mDisplayController.getDisplayLayout(displayId)); + setDisplayLayout(mDisplayController.getDisplayLayout(displayId)); } + /** + * A callback for any observed transition that contains a display change in its + * {@link android.window.TransitionRequestInfo}, + */ @Override - public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + public void onDisplayChange(int displayId, int fromRotation, int toRotation, + @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction t) { if (displayId != mPipDisplayLayoutState.getDisplayId()) { return; } - onDisplayChanged(mDisplayController.getDisplayLayout(displayId)); + final float snapFraction = mPipBoundsAlgorithm.getSnapFraction(mPipBoundsState.getBounds()); + final float boundsScale = mPipBoundsState.getBoundsScale(); + + // Update the display layout caches even if we are not in PiP. + setDisplayLayout(mDisplayController.getDisplayLayout(displayId)); + + if (!mPipTransitionState.isInPip()) { + return; + } + + mPipTouchHandler.updateMinMaxSize(mPipBoundsState.getAspectRatio()); + + // Update the caches to reflect the new display layout in the movement bounds; + // temporarily update bounds to be at the top left for the movement bounds calculation. + Rect toBounds = new Rect(0, 0, + (int) Math.ceil(mPipBoundsState.getMaxSize().x * boundsScale), + (int) Math.ceil(mPipBoundsState.getMaxSize().y * boundsScale)); + mPipBoundsState.setBounds(toBounds); + mPipTouchHandler.updateMovementBounds(); + + // The policy is to keep PiP snap fraction invariant. + mPipBoundsAlgorithm.applySnapFraction(toBounds, snapFraction); + mPipBoundsState.setBounds(toBounds); + t.setBounds(mPipTransitionState.mPipTaskToken, toBounds); } - private void onDisplayChanged(DisplayLayout layout) { + private void setDisplayLayout(DisplayLayout layout) { mPipDisplayLayoutState.setDisplayLayout(layout); } @@ -279,6 +324,10 @@ public class PipController implements ConfigurationChangeListener, int launcherRotation, Rect hotseatKeepClearArea) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "getSwipePipToHomeBounds: %s", componentName); + // preemptively add the keep clear area for Hotseat, so that it is taken into account + // when calculating the entry destination bounds of PiP window + mPipBoundsState.setNamedUnrestrictedKeepClearArea( + PipBoundsState.NAMED_KCA_LAUNCHER_SHELF, hotseatKeepClearArea); mPipBoundsState.setBoundsStateForEntry(componentName, activityInfo, pictureInPictureParams, mPipBoundsAlgorithm); return mPipBoundsAlgorithm.getEntryDestinationBounds(); @@ -307,25 +356,53 @@ public class PipController implements ConfigurationChangeListener, mPipRecentsAnimationListener.onPipAnimationStarted(); } + private void setLauncherKeepClearAreaHeight(boolean visible, int height) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "setLauncherKeepClearAreaHeight: visible=%b, height=%d", visible, height); + if (visible) { + Rect rect = new Rect( + 0, mPipDisplayLayoutState.getDisplayBounds().bottom - height, + mPipDisplayLayoutState.getDisplayBounds().right, + mPipDisplayLayoutState.getDisplayBounds().bottom); + mPipBoundsState.setNamedUnrestrictedKeepClearArea( + PipBoundsState.NAMED_KCA_LAUNCHER_SHELF, rect); + } else { + mPipBoundsState.setNamedUnrestrictedKeepClearArea( + PipBoundsState.NAMED_KCA_LAUNCHER_SHELF, null); + } + mPipTouchHandler.onShelfVisibilityChanged(visible, height); + } + @Override public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) { - if (newState == PipTransitionState.SWIPING_TO_PIP) { - Preconditions.checkState(extra != null, - "No extra bundle for " + mPipTransitionState); - - SurfaceControl overlay = extra.getParcelable( - SWIPE_TO_PIP_OVERLAY, SurfaceControl.class); - Rect appBounds = extra.getParcelable( - SWIPE_TO_PIP_APP_BOUNDS, Rect.class); - - Preconditions.checkState(appBounds != null, - "App bounds can't be null for " + mPipTransitionState); - mPipTransitionState.setSwipePipToHomeState(overlay, appBounds); - } else if (newState == PipTransitionState.ENTERED_PIP) { - if (mPipTransitionState.isInSwipePipToHomeTransition()) { - mPipTransitionState.resetSwipePipToHomeState(); - } + switch (newState) { + case PipTransitionState.SWIPING_TO_PIP: + Preconditions.checkState(extra != null, + "No extra bundle for " + mPipTransitionState); + + SurfaceControl overlay = extra.getParcelable( + SWIPE_TO_PIP_OVERLAY, SurfaceControl.class); + Rect appBounds = extra.getParcelable( + SWIPE_TO_PIP_APP_BOUNDS, Rect.class); + + Preconditions.checkState(appBounds != null, + "App bounds can't be null for " + mPipTransitionState); + mPipTransitionState.setSwipePipToHomeState(overlay, appBounds); + break; + case PipTransitionState.ENTERED_PIP: + if (mPipTransitionState.isInSwipePipToHomeTransition()) { + mPipTransitionState.resetSwipePipToHomeState(); + } + if (mOnIsInPipStateChangedListener != null) { + mOnIsInPipStateChangedListener.accept(true /* inPip */); + } + break; + case PipTransitionState.EXITED_PIP: + if (mOnIsInPipStateChangedListener != null) { + mOnIsInPipStateChangedListener.accept(false /* inPip */); + } + break; } } @@ -352,6 +429,49 @@ public class PipController implements ConfigurationChangeListener, mPipBoundsAlgorithm.dump(pw, innerPrefix); mPipBoundsState.dump(pw, innerPrefix); mPipDisplayLayoutState.dump(pw, innerPrefix); + mPipTransitionState.dump(pw, innerPrefix); + } + + private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { + mOnIsInPipStateChangedListener = callback; + if (mOnIsInPipStateChangedListener != null) { + callback.accept(mPipTransitionState.isInPip()); + } + } + + /** + * The interface for calls from outside the Shell, within the host process. + */ + public class PipImpl implements Pip { + @Override + public void expandPip() {} + + @Override + public void onSystemUiStateChanged(boolean isSysUiStateValid, long flag) {} + + @Override + public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { + mMainExecutor.execute(() -> { + PipController.this.setOnIsInPipStateChangedListener(callback); + }); + } + + @Override + public void addPipExclusionBoundsChangeListener(Consumer<Rect> listener) { + mMainExecutor.execute(() -> { + mPipBoundsState.addPipExclusionBoundsChangeCallback(listener); + }); + } + + @Override + public void removePipExclusionBoundsChangeListener(Consumer<Rect> listener) { + mMainExecutor.execute(() -> { + mPipBoundsState.removePipExclusionBoundsChangeCallback(listener); + }); + } + + @Override + public void showPictureInPictureMenu() {} } /** @@ -428,7 +548,10 @@ public class PipController implements ConfigurationChangeListener, public void setShelfHeight(boolean visible, int height) {} @Override - public void setLauncherKeepClearAreaHeight(boolean visible, int height) {} + public void setLauncherKeepClearAreaHeight(boolean visible, int height) { + executeRemoteCallWithTaskPermission(mController, "setLauncherKeepClearAreaHeight", + (controller) -> controller.setLauncherKeepClearAreaHeight(visible, height)); + } @Override public void setLauncherAppIconSize(int iconSizePx) {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java index e7e797096c0e..b3070f29c6e2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java @@ -35,10 +35,10 @@ import androidx.annotation.NonNull; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.DismissViewUtils; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.bubbles.DismissCircleView; -import com.android.wm.shell.common.bubbles.DismissView; -import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import com.android.wm.shell.common.pip.PipUiEventLogger; +import com.android.wm.shell.shared.bubbles.DismissCircleView; +import com.android.wm.shell.shared.bubbles.DismissView; +import com.android.wm.shell.shared.magnetictarget.MagnetizedObject; import kotlin.Unit; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java index b757b00f16dd..ffda56d89276 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java @@ -28,7 +28,7 @@ import android.view.IWindowManager; import android.view.InputChannel; import android.view.InputEvent; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.protolog.ShellProtoLogGroup; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java index 42b8e9f5a3ad..a29104c4aafd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java @@ -59,13 +59,13 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import android.widget.LinearLayout; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; -import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.animation.Interpolators; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java index 495cd0075494..0324fdba0fbf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java @@ -16,6 +16,7 @@ package com.android.wm.shell.pip2.phone; +import static androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY; import static androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_NO_BOUNCY; import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW; import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_MEDIUM; @@ -25,6 +26,7 @@ import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_NONE; import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_RIGHT; import static com.android.wm.shell.pip2.phone.PipMenuView.ANIM_TYPE_DISMISS; import static com.android.wm.shell.pip2.phone.PipMenuView.ANIM_TYPE_NONE; +import static com.android.wm.shell.pip2.phone.PipTransition.ANIMATING_BOUNDS_CHANGE_DURATION; import android.annotation.NonNull; import android.annotation.Nullable; @@ -35,24 +37,25 @@ import android.os.Bundle; import android.os.Debug; import android.view.SurfaceControl; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; +import com.android.internal.util.Preconditions; import com.android.wm.shell.R; import com.android.wm.shell.animation.FloatProperties; import com.android.wm.shell.common.FloatingContentCoordinator; -import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import com.android.wm.shell.common.pip.PipAppOpsListener; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipPerfHintController; import com.android.wm.shell.common.pip.PipSnapAlgorithm; +import com.android.wm.shell.pip2.animation.PipResizeAnimator; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.animation.PhysicsAnimator; +import com.android.wm.shell.shared.magnetictarget.MagnetizedObject; import kotlin.Unit; import kotlin.jvm.functions.Function0; import java.util.Optional; -import java.util.function.Consumer; /** * A helper to animate and manipulate the PiP. @@ -62,6 +65,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, PipTransitionState.PipTransitionStateChangedListener { private static final String TAG = "PipMotionHelper"; private static final String FLING_BOUNDS_CHANGE = "fling_bounds_change"; + private static final String ANIMATING_BOUNDS_CHANGE = "animating_bounds_change"; private static final boolean DEBUG = false; private static final int SHRINK_STACK_FROM_MENU_DURATION = 250; @@ -113,7 +117,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, /** SpringConfig to use for fling-then-spring animations. */ private final PhysicsAnimator.SpringConfig mSpringConfig = - new PhysicsAnimator.SpringConfig(700f, DAMPING_RATIO_NO_BOUNCY); + new PhysicsAnimator.SpringConfig(300f, DAMPING_RATIO_LOW_BOUNCY); /** SpringConfig used for animating into the dismiss region, matches the one in * {@link MagnetizedObject}. */ @@ -129,15 +133,6 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, private final PhysicsAnimator.SpringConfig mConflictResolutionSpringConfig = new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_NO_BOUNCY); - private final Consumer<Rect> mUpdateBoundsCallback = (Rect newBounds) -> { - if (mPipBoundsState.getBounds().equals(newBounds)) { - return; - } - - mMenuController.updateMenuLayout(newBounds); - mPipBoundsState.setBounds(newBounds); - }; - /** * Whether we're springing to the touch event location (vs. moving it to that position * instantly). We spring-to-touch after PIP is dragged out of the magnetic target, since it was @@ -152,9 +147,16 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, private boolean mDismissalPending = false; /** - * Set to true if bounds change transition has been scheduled from PipMotionHelper. + * Set to true if bounds change transition has been scheduled from PipMotionHelper + * after animating is over. + */ + private boolean mWaitingForFlingTransition = false; + + /** + * Set to true if bounds change transition has been scheduled from PipMotionHelper, + * and if the animation is supposed to run while transition is playing. */ - private boolean mWaitingForBoundsChangeTransition = false; + private boolean mWaitingToPlayBoundsChangeTransition = false; /** * Gets set in {@link #animateToExpandedState(Rect, Rect, Rect, Runnable)}, this callback is @@ -404,6 +406,17 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, // location now. mSpringingToTouch = false; + // Boost the velocityX if it's zero to forcefully push it towards the nearest edge. + // We don't simply change the xEndValue below since the PhysicsAnimator would rely on the + // same velocityX to find out which edge to snap to. + if (velocityX == 0) { + final int motionCenterX = mPipBoundsState + .getMotionBoundsState().getBoundsInMotion().centerX(); + final int displayCenterX = mPipBoundsState + .getDisplayBounds().centerX(); + velocityX = (motionCenterX < displayCenterX) ? -0.001f : 0.001f; + } + mTemporaryBoundsPhysicsAnimator .spring(FloatProperties.RECT_WIDTH, getBounds().width(), mSpringConfig) .spring(FloatProperties.RECT_HEIGHT, getBounds().height(), mSpringConfig) @@ -543,11 +556,20 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, + " callers=\n%s", TAG, originalBounds, offset, Debug.getCallers(5, " ")); } + if (offset == 0) { + return; + } + cancelPhysicsAnimation(); - /* - mPipTaskOrganizer.scheduleOffsetPip(originalBounds, offset, SHIFT_DURATION, - mUpdateBoundsCallback); - */ + + Rect adjustedBounds = new Rect(originalBounds); + adjustedBounds.offset(0, offset); + + setAnimatingToBounds(adjustedBounds); + Bundle extra = new Bundle(); + extra.putBoolean(ANIMATING_BOUNDS_CHANGE, true); + extra.putInt(ANIMATING_BOUNDS_CHANGE_DURATION, SHIFT_DURATION); + mPipTransitionState.setState(PipTransitionState.SCHEDULED_BOUNDS_CHANGE, extra); } /** @@ -562,11 +584,11 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, /** Set new fling configs whose min/max values respect the given movement bounds. */ private void rebuildFlingConfigs() { mFlingConfigX = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION, - mPipBoundsAlgorithm.getMovementBounds(getBounds()).left, - mPipBoundsAlgorithm.getMovementBounds(getBounds()).right); + mPipBoundsState.getMovementBounds().left, + mPipBoundsState.getMovementBounds().right); mFlingConfigY = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION, - mPipBoundsAlgorithm.getMovementBounds(getBounds()).top, - mPipBoundsAlgorithm.getMovementBounds(getBounds()).bottom); + mPipBoundsState.getMovementBounds().top, + mPipBoundsState.getMovementBounds().bottom); final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets(); mStashConfigX = new PhysicsAnimator.FlingConfig( DEFAULT_FRICTION, @@ -634,6 +656,9 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, // The physics animation ended, though we may not necessarily be done animating, such as // when we're still dragging after moving out of the magnetic target. if (!mDismissalPending && !mSpringingToTouch && !mMagnetizedPip.getObjectStuckToTarget()) { + // Update the earlier estimate on bounds we are animating towards, since physics + // animator is non-deterministic. + setAnimatingToBounds(mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); // do not schedule resize if PiP is dismissing, which may cause app re-open to // mBounds instead of its normal bounds. Bundle extra = new Bundle(); @@ -672,7 +697,12 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, /** * Directly resizes the PiP to the given {@param bounds}. */ - private void resizeAndAnimatePipUnchecked(Rect toBounds, int duration) { + void resizeAndAnimatePipUnchecked(Rect toBounds, int duration) { + if (mPipBoundsState.getMotionBoundsState().isInMotion()) { + // Do not carry out any resizing if we are dragging or physics animator is running. + return; + } + if (DEBUG) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: resizeAndAnimatePipUnchecked: toBounds=%s" @@ -682,10 +712,11 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, // Intentionally resize here even if the current bounds match the destination bounds. // This is so all the proper callbacks are performed. - - // mPipTaskOrganizer.scheduleAnimateResizePip(toBounds, duration, - // TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND, null /* updateBoundsCallback */); - // setAnimatingToBounds(toBounds); + setAnimatingToBounds(toBounds); + Bundle extra = new Bundle(); + extra.putBoolean(ANIMATING_BOUNDS_CHANGE, true); + extra.putInt(ANIMATING_BOUNDS_CHANGE_DURATION, duration); + mPipTransitionState.setState(PipTransitionState.SCHEDULED_BOUNDS_CHANGE, extra); } @Override @@ -694,7 +725,11 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, @Nullable Bundle extra) { switch (newState) { case PipTransitionState.SCHEDULED_BOUNDS_CHANGE: - if (!extra.getBoolean(FLING_BOUNDS_CHANGE)) break; + mWaitingForFlingTransition = extra.getBoolean(FLING_BOUNDS_CHANGE); + mWaitingToPlayBoundsChangeTransition = extra.getBoolean(ANIMATING_BOUNDS_CHANGE); + if (!mWaitingForFlingTransition && !mWaitingToPlayBoundsChangeTransition) { + break; + } if (mPipBoundsState.getBounds().equals( mPipBoundsState.getMotionBoundsState().getBoundsInMotion())) { @@ -709,30 +744,30 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, break; } - // If touch is turned off and we are in a fling animation, schedule a transition. - mWaitingForBoundsChangeTransition = true; + // Delay config until the end, if we are animating after scheduling the transition. mPipScheduler.scheduleAnimateResizePip( - mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); + mPipBoundsState.getMotionBoundsState().getAnimatingToBounds(), + mWaitingToPlayBoundsChangeTransition, + extra.getInt(ANIMATING_BOUNDS_CHANGE_DURATION, + PipTransition.BOUNDS_CHANGE_JUMPCUT_DURATION)); break; case PipTransitionState.CHANGING_PIP_BOUNDS: - if (!mWaitingForBoundsChangeTransition) break; - - // If bounds change transition was scheduled from this class, handle leash updates. - mWaitingForBoundsChangeTransition = false; SurfaceControl.Transaction startTx = extra.getParcelable( PipTransition.PIP_START_TX, SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishTx = extra.getParcelable( + PipTransition.PIP_FINISH_TX, SurfaceControl.Transaction.class); Rect destinationBounds = extra.getParcelable( PipTransition.PIP_DESTINATION_BOUNDS, Rect.class); - startTx.setPosition(mPipTransitionState.mPinnedTaskLeash, - destinationBounds.left, destinationBounds.top); - startTx.apply(); - - // All motion operations have actually finished, so make bounds cache updates. - settlePipBoundsAfterPhysicsAnimation(false /* animatingAfter */); - cleanUpHighPerfSessionMaybe(); - - // Signal that the transition is done - should update transition state by default. - mPipScheduler.scheduleFinishResizePip(false /* configAtEnd */); + final int duration = extra.getInt(ANIMATING_BOUNDS_CHANGE_DURATION, + PipTransition.BOUNDS_CHANGE_JUMPCUT_DURATION); + + if (mWaitingForFlingTransition) { + mWaitingForFlingTransition = false; + handleFlingTransition(startTx, finishTx, destinationBounds); + } else if (mWaitingToPlayBoundsChangeTransition) { + mWaitingToPlayBoundsChangeTransition = false; + startResizeAnimation(startTx, finishTx, destinationBounds, duration); + } break; case PipTransitionState.EXITING_PIP: // We need to force finish any local animators if about to leave PiP, to avoid @@ -740,20 +775,80 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, if (!mPipBoundsState.getMotionBoundsState().isInMotion()) break; cancelPhysicsAnimation(); settlePipBoundsAfterPhysicsAnimation(false /* animatingAfter */); + break; } } + private void handleFlingTransition(SurfaceControl.Transaction startTx, + SurfaceControl.Transaction finishTx, Rect destinationBounds) { + startTx.setPosition(mPipTransitionState.mPinnedTaskLeash, + destinationBounds.left, destinationBounds.top); + startTx.apply(); + + // All motion operations have actually finished, so make bounds cache updates. + settlePipBoundsAfterPhysicsAnimation(false /* animatingAfter */); + cleanUpHighPerfSessionMaybe(); + + // Signal that the transition is done - should update transition state by default. + mPipScheduler.scheduleFinishResizePip(destinationBounds, false /* configAtEnd */); + } + + private void startResizeAnimation(SurfaceControl.Transaction startTx, + SurfaceControl.Transaction finishTx, Rect destinationBounds, int duration) { + SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash; + Preconditions.checkState(pipLeash != null, + "No leash cached by mPipTransitionState=" + mPipTransitionState); + + startTx.setWindowCrop(pipLeash, mPipBoundsState.getBounds().width(), + mPipBoundsState.getBounds().height()); + + PipResizeAnimator animator = new PipResizeAnimator(mContext, pipLeash, + startTx, finishTx, mPipBoundsState.getBounds(), mPipBoundsState.getBounds(), + destinationBounds, duration, 0f /* angle */); + animator.setAnimationEndCallback(() -> { + // In case an ongoing drag/fling was present before a deterministic resize transition + // kicked in, we need to update the update bounds properly before cleaning in-motion + // state. + mPipBoundsState.getMotionBoundsState().setBoundsInMotion(destinationBounds); + settlePipBoundsAfterPhysicsAnimation(false /* animatingAfter */); + + cleanUpHighPerfSessionMaybe(); + // Signal that we are done with resize transition + mPipScheduler.scheduleFinishResizePip(destinationBounds, true /* configAtEnd */); + }); + animator.start(); + } + private void settlePipBoundsAfterPhysicsAnimation(boolean animatingAfter) { - if (!animatingAfter) { + if (!animatingAfter && mPipBoundsState.getMotionBoundsState().isInMotion()) { // The physics animation ended, though we may not necessarily be done animating, such as // when we're still dragging after moving out of the magnetic target. Only set the final // bounds state and clear motion bounds completely if the whole animation is over. - mPipBoundsState.setBounds(mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded(); } mPipBoundsState.getMotionBoundsState().onPhysicsAnimationEnded(); mSpringingToTouch = false; mDismissalPending = false; + + // Check whether new bounds after fling imply we need to update stash state too. + stashEndActionIfNeeded(); + } + + private void stashEndActionIfNeeded() { + boolean isStashing = mPipBoundsState.getBounds().right > mPipBoundsState + .getDisplayBounds().width() || mPipBoundsState.getBounds().left < 0; + if (!isStashing) { + return; + } + + if (mPipBoundsState.getBounds().left < 0 + && mPipBoundsState.getStashedState() != STASH_TYPE_LEFT) { + mPipBoundsState.setStashed(STASH_TYPE_LEFT); + } else if (mPipBoundsState.getBounds().left >= 0 + && mPipBoundsState.getStashedState() != STASH_TYPE_RIGHT) { + mPipBoundsState.setStashed(STASH_TYPE_RIGHT); + } + mMenuController.hideMenu(); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java index 33e80bd80988..f5ef64dff94b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java @@ -16,6 +16,7 @@ package com.android.wm.shell.pip2.phone; import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_NONE; +import static com.android.wm.shell.pip2.phone.PipTransition.ANIMATING_BOUNDS_CHANGE_DURATION; import android.annotation.Nullable; import android.content.Context; @@ -49,7 +50,6 @@ import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.pip2.animation.PipResizeAnimator; import java.io.PrintWriter; -import java.util.function.Consumer; /** * Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to @@ -85,8 +85,6 @@ public class PipResizeGestureHandler implements private final Rect mUserResizeBounds = new Rect(); private final Rect mDownBounds = new Rect(); private final Rect mStartBoundsAfterRelease = new Rect(); - private final Runnable mUpdateMovementBoundsRunnable; - private final Consumer<Rect> mUpdateResizeBoundsCallback; private float mTouchSlop; @@ -120,7 +118,6 @@ public class PipResizeGestureHandler implements PipTouchState pipTouchState, PipScheduler pipScheduler, PipTransitionState pipTransitionState, - Runnable updateMovementBoundsRunnable, PipUiEventLogger pipUiEventLogger, PhonePipMenuController menuActivityController, ShellExecutor mainExecutor, @@ -137,18 +134,9 @@ public class PipResizeGestureHandler implements mPipTransitionState = pipTransitionState; mPipTransitionState.addPipTransitionStateChangedListener(this); - mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; mPhonePipMenuController = menuActivityController; mPipUiEventLogger = pipUiEventLogger; mPinchResizingAlgorithm = new PipPinchResizingAlgorithm(); - - mUpdateResizeBoundsCallback = (rect) -> { - mUserResizeBounds.set(rect); - // mMotionHelper.synchronizePinnedStackBounds(); - mUpdateMovementBoundsRunnable.run(); - mPipBoundsState.setBounds(rect); - resetState(); - }; } void init() { @@ -535,7 +523,8 @@ public class PipResizeGestureHandler implements mWaitingForBoundsChangeTransition = true; // Schedule PiP resize transition, but delay any config updates until very end. - mPipScheduler.scheduleAnimateResizePip(mLastResizeBounds, true /* configAtEnd */); + mPipScheduler.scheduleAnimateResizePip(mLastResizeBounds, + true /* configAtEnd */, PINCH_RESIZE_SNAP_DURATION); break; case PipTransitionState.CHANGING_PIP_BOUNDS: if (!mWaitingForBoundsChangeTransition) break; @@ -550,19 +539,24 @@ public class PipResizeGestureHandler implements PipTransition.PIP_START_TX, SurfaceControl.Transaction.class); SurfaceControl.Transaction finishTx = extra.getParcelable( PipTransition.PIP_FINISH_TX, SurfaceControl.Transaction.class); + final int duration = extra.getInt(ANIMATING_BOUNDS_CHANGE_DURATION, + PipTransition.BOUNDS_CHANGE_JUMPCUT_DURATION); + startTx.setWindowCrop(pipLeash, mPipBoundsState.getBounds().width(), mPipBoundsState.getBounds().height()); PipResizeAnimator animator = new PipResizeAnimator(mContext, pipLeash, startTx, finishTx, mPipBoundsState.getBounds(), mStartBoundsAfterRelease, - mLastResizeBounds, PINCH_RESIZE_SNAP_DURATION, mAngle); + mLastResizeBounds, duration, mAngle); animator.setAnimationEndCallback(() -> { // All motion operations have actually finished, so make bounds cache updates. - mUpdateResizeBoundsCallback.accept(mLastResizeBounds); + mUserResizeBounds.set(mLastResizeBounds); + resetState(); cleanUpHighPerfSessionMaybe(); // Signal that we are done with resize transition - mPipScheduler.scheduleFinishResizePip(true /* configAtEnd */); + mPipScheduler.scheduleFinishResizePip( + mLastResizeBounds, true /* configAtEnd */); }); animator.start(); break; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index 9c1e321a1273..f4defdc7963c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -33,7 +33,7 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipUtils; @@ -52,11 +52,14 @@ public class PipScheduler { private final Context mContext; private final PipBoundsState mPipBoundsState; + private final PhonePipMenuController mPipMenuController; private final ShellExecutor mMainExecutor; private final PipTransitionState mPipTransitionState; private PipSchedulerReceiver mSchedulerReceiver; private PipTransitionController mPipTransitionController; + @Nullable private Runnable mUpdateMovementBoundsRunnable; + /** * Temporary PiP CUJ codes to schedule PiP related transitions directly from Shell. * This is used for a broadcast receiver to resolve intents. This should be removed once @@ -94,10 +97,12 @@ public class PipScheduler { public PipScheduler(Context context, PipBoundsState pipBoundsState, + PhonePipMenuController pipMenuController, ShellExecutor mainExecutor, PipTransitionState pipTransitionState) { mContext = context; mPipBoundsState = pipBoundsState; + mPipMenuController = pipMenuController; mMainExecutor = mainExecutor; mPipTransitionState = pipTransitionState; @@ -162,6 +167,18 @@ public class PipScheduler { * @param configAtEnd true if we are delaying config updates until the transition ends. */ public void scheduleAnimateResizePip(Rect toBounds, boolean configAtEnd) { + scheduleAnimateResizePip(toBounds, configAtEnd, + PipTransition.BOUNDS_CHANGE_JUMPCUT_DURATION); + } + + /** + * Animates resizing of the pinned stack given the duration. + * + * @param configAtEnd true if we are delaying config updates until the transition ends. + * @param duration the suggested duration to run the animation; the component responsible + * for running the animator will get this as an extra. + */ + public void scheduleAnimateResizePip(Rect toBounds, boolean configAtEnd, int duration) { if (mPipTransitionState.mPipTaskToken == null || !mPipTransitionState.isInPip()) { return; } @@ -170,16 +187,20 @@ public class PipScheduler { if (configAtEnd) { wct.deferConfigToTransitionEnd(mPipTransitionState.mPipTaskToken); } - mPipTransitionController.startResizeTransition(wct); + mPipTransitionController.startResizeTransition(wct, duration); } /** * Signals to Core to finish the PiP resize transition. * Note that we do not allow any actual WM Core changes at this point. * + * @param toBounds destination bounds used only for internal state updates - not sent to Core. * @param configAtEnd true if we are waiting for config updates at the end of the transition. */ - public void scheduleFinishResizePip(boolean configAtEnd) { + public void scheduleFinishResizePip(Rect toBounds, boolean configAtEnd) { + // Make updates to the internal state to reflect new bounds + onFinishingPipResize(toBounds); + SurfaceControl.Transaction tx = null; if (configAtEnd) { tx = new SurfaceControl.Transaction(); @@ -226,4 +247,23 @@ public class PipScheduler { tx.setMatrix(leash, transformTensor, mMatrixTmp); tx.apply(); } + + void setUpdateMovementBoundsRunnable(Runnable updateMovementBoundsRunnable) { + mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; + } + + private void maybeUpdateMovementBounds() { + if (mUpdateMovementBoundsRunnable != null) { + mUpdateMovementBoundsRunnable.run(); + } + } + + private void onFinishingPipResize(Rect newBounds) { + if (mPipBoundsState.getBounds().equals(newBounds)) { + return; + } + mPipBoundsState.setBounds(newBounds); + mPipMenuController.updateMenuLayout(newBounds); + maybeUpdateMovementBounds(); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java new file mode 100644 index 000000000000..262c14d2bfe3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2024 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.pip2.phone; + +import static com.android.wm.shell.pip2.phone.PipTransition.ANIMATING_BOUNDS_CHANGE_DURATION; + +import android.app.ActivityManager; +import android.app.PictureInPictureParams; +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.view.SurfaceControl; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.util.Preconditions; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.pip.PipBoundsAlgorithm; +import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipUtils; +import com.android.wm.shell.pip2.animation.PipResizeAnimator; +import com.android.wm.shell.shared.annotations.ShellMainThread; + +/** + * A Task Listener implementation used only for CUJs and trigger paths that cannot be initiated via + * Transitions framework directly. + * Hence, it's the intention to keep the usage of this class for a very limited set of cases. + */ +public class PipTaskListener implements ShellTaskOrganizer.TaskListener, + PipTransitionState.PipTransitionStateChangedListener { + private static final int ASPECT_RATIO_CHANGE_DURATION = 250; + private static final String ANIMATING_ASPECT_RATIO_CHANGE = "animating_aspect_ratio_change"; + + private final Context mContext; + private final PipTransitionState mPipTransitionState; + private final PipScheduler mPipScheduler; + private final PipBoundsState mPipBoundsState; + private final PipBoundsAlgorithm mPipBoundsAlgorithm; + private final ShellExecutor mMainExecutor; + private final PictureInPictureParams mPictureInPictureParams = + new PictureInPictureParams.Builder().build(); + + private boolean mWaitingForAspectRatioChange = false; + + public PipTaskListener(Context context, + ShellTaskOrganizer shellTaskOrganizer, + PipTransitionState pipTransitionState, + PipScheduler pipScheduler, + PipBoundsState pipBoundsState, + PipBoundsAlgorithm pipBoundsAlgorithm, + @ShellMainThread ShellExecutor mainExecutor) { + mContext = context; + mPipTransitionState = pipTransitionState; + mPipScheduler = pipScheduler; + mPipBoundsState = pipBoundsState; + mPipBoundsAlgorithm = pipBoundsAlgorithm; + mMainExecutor = mainExecutor; + + mPipTransitionState.addPipTransitionStateChangedListener(this); + if (PipUtils.isPip2ExperimentEnabled()) { + mMainExecutor.execute(() -> { + shellTaskOrganizer.addListenerForType(this, + ShellTaskOrganizer.TASK_LISTENER_TYPE_PIP); + }); + } + } + + void setPictureInPictureParams(@Nullable PictureInPictureParams params) { + if (mPictureInPictureParams.equals(params)) { + return; + } + mPictureInPictureParams.copyOnlySet(params != null ? params + : new PictureInPictureParams.Builder().build()); + } + + @NonNull + public PictureInPictureParams getPictureInPictureParams() { + return mPictureInPictureParams; + } + + @Override + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + PictureInPictureParams params = taskInfo.pictureInPictureParams; + if (mPictureInPictureParams.equals(params)) { + return; + } + setPictureInPictureParams(params); + float newAspectRatio = mPictureInPictureParams.getAspectRatioFloat(); + if (PipUtils.aspectRatioChanged(newAspectRatio, mPipBoundsState.getAspectRatio())) { + mPipTransitionState.setOnIdlePipTransitionStateRunnable(() -> { + onAspectRatioChanged(newAspectRatio); + }); + } + } + + private void onAspectRatioChanged(float ratio) { + mPipBoundsState.setAspectRatio(ratio); + + final Rect destinationBounds = mPipBoundsAlgorithm.getAdjustedDestinationBounds( + mPipBoundsState.getBounds(), mPipBoundsState.getAspectRatio()); + // Avoid scheduling a resize transition if destination bounds are unchanged, otherise + // we could end up with a no-op transition. + if (!destinationBounds.equals(mPipBoundsState.getBounds())) { + Bundle extra = new Bundle(); + extra.putBoolean(ANIMATING_ASPECT_RATIO_CHANGE, true); + mPipTransitionState.setState(PipTransitionState.SCHEDULED_BOUNDS_CHANGE, extra); + } + } + + @Override + public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, + @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) { + switch (newState) { + case PipTransitionState.SCHEDULED_BOUNDS_CHANGE: + mWaitingForAspectRatioChange = extra.getBoolean(ANIMATING_ASPECT_RATIO_CHANGE); + if (!mWaitingForAspectRatioChange) break; + + mPipScheduler.scheduleAnimateResizePip( + mPipBoundsAlgorithm.getAdjustedDestinationBounds( + mPipBoundsState.getBounds(), mPipBoundsState.getAspectRatio()), + false /* configAtEnd */, ASPECT_RATIO_CHANGE_DURATION); + break; + case PipTransitionState.CHANGING_PIP_BOUNDS: + final SurfaceControl.Transaction startTx = extra.getParcelable( + PipTransition.PIP_START_TX, SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishTx = extra.getParcelable( + PipTransition.PIP_FINISH_TX, SurfaceControl.Transaction.class); + final Rect destinationBounds = extra.getParcelable( + PipTransition.PIP_DESTINATION_BOUNDS, Rect.class); + final int duration = extra.getInt(ANIMATING_BOUNDS_CHANGE_DURATION, + PipTransition.BOUNDS_CHANGE_JUMPCUT_DURATION); + + Preconditions.checkNotNull(mPipTransitionState.mPinnedTaskLeash, + "Leash is null for bounds transition."); + + if (mWaitingForAspectRatioChange) { + PipResizeAnimator animator = new PipResizeAnimator(mContext, + mPipTransitionState.mPinnedTaskLeash, startTx, finishTx, + destinationBounds, + mPipBoundsState.getBounds(), destinationBounds, duration, + 0f /* delta */); + animator.setAnimationEndCallback(() -> { + mPipScheduler.scheduleFinishResizePip( + destinationBounds, false /* configAtEnd */); + }); + animator.start(); + } + break; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java index 56a465a4889a..029f001401c5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java @@ -38,6 +38,7 @@ import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.os.Bundle; +import android.os.SystemProperties; import android.provider.DeviceConfig; import android.util.Size; import android.view.DisplayCutout; @@ -51,7 +52,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; @@ -78,6 +79,8 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha private static final String TAG = "PipTouchHandler"; private static final float DEFAULT_STASH_VELOCITY_THRESHOLD = 18000.f; + private static final long PIP_KEEP_CLEAR_AREAS_DELAY = + SystemProperties.getLong("persist.wm.debug.pip_keep_clear_areas_delay", 200); // Allow PIP to resize to a slightly bigger state upon touch private boolean mEnableResize; @@ -106,9 +109,6 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha private float mStashVelocityThreshold; - // The reference inset bounds, used to determine the dismiss fraction - private final Rect mInsetBounds = new Rect(); - // Used to workaround an issue where the WM rotation happens before we are notified, allowing // us to send stale bounds private int mDeferResizeToNormalBoundsUntilRotation = -1; @@ -137,6 +137,10 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha // Temp vars private final Rect mTmpBounds = new Rect(); + // Callbacks + private final Runnable mMoveOnShelVisibilityChanged; + + /** * A listener for the PIP menu activity. */ @@ -202,30 +206,44 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha mMenuController.addListener(new PipMenuListener()); mGesture = new DefaultPipTouchGesture(); mMotionHelper = pipMotionHelper; + mPipScheduler.setUpdateMovementBoundsRunnable(this::updateMovementBounds); mPipDismissTargetHandler = new PipDismissTargetHandler(context, pipUiEventLogger, mMotionHelper, mainExecutor); mTouchState = new PipTouchState(ViewConfiguration.get(context), () -> { - if (mPipBoundsState.isStashed()) { - animateToUnStashedState(); - mPipUiEventLogger.log( - PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_UNSTASHED); - mPipBoundsState.setStashed(STASH_TYPE_NONE); - } else { - mMenuController.showMenuWithPossibleDelay(MENU_STATE_FULL, - mPipBoundsState.getBounds(), true /* allowMenuTimeout */, - willResizeMenu(), - shouldShowResizeHandle()); - } + mMenuController.showMenuWithPossibleDelay(MENU_STATE_FULL, + mPipBoundsState.getBounds(), true /* allowMenuTimeout */, + willResizeMenu(), + shouldShowResizeHandle()); }, menuController::hideMenu, mainExecutor); mPipResizeGestureHandler = new PipResizeGestureHandler(context, pipBoundsAlgorithm, - pipBoundsState, mTouchState, mPipScheduler, mPipTransitionState, - this::updateMovementBounds, pipUiEventLogger, menuController, mainExecutor, + pipBoundsState, mTouchState, mPipScheduler, mPipTransitionState, pipUiEventLogger, + menuController, mainExecutor, mPipPerfHintController); mPipBoundsState.addOnAspectRatioChangedCallback(this::updateMinMaxSize); + mMoveOnShelVisibilityChanged = () -> { + if (mIsImeShowing && mImeHeight > mShelfHeight) { + // Early bail-out if IME is visible with a larger height present; + // this should block unnecessary PiP movement since we delay checking for + // KCA triggered movement to wait for other transitions (e.g. due to IME changes). + return; + } + mPipTransitionState.setOnIdlePipTransitionStateRunnable(() -> { + boolean hasUserInteracted = (mPipBoundsState.hasUserMovedPip() + || mPipBoundsState.hasUserResizedPip()); + int delta = mPipBoundsAlgorithm.getEntryDestinationBounds().top + - mPipBoundsState.getBounds().top; + + if (!mIsImeShowing && !hasUserInteracted && delta != 0) { + // If the user hasn't interacted with PiP, we respect the keep clear areas + mMotionHelper.animateToOffset(mPipBoundsState.getBounds(), delta); + } + }); + }; + if (PipUtils.isPip2ExperimentEnabled()) { shellInit.addInitCallback(this::onInit, this); } @@ -327,6 +345,8 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha mFloatingContentCoordinator.onContentRemoved(mMotionHelper); mPipResizeGestureHandler.onActivityUnpinned(); mPipInputConsumer.unregisterInputConsumer(); + mPipBoundsState.setHasUserMovedPip(false); + mPipBoundsState.setHasUserResizedPip(false); } void onPinnedStackAnimationEnded( @@ -356,11 +376,42 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { mIsImeShowing = imeVisible; mImeHeight = imeHeight; + + // Cache new movement bounds using the new potential IME height. + updateMovementBounds(); + + mPipTransitionState.setOnIdlePipTransitionStateRunnable(() -> { + int delta = mPipBoundsState.getMovementBounds().bottom + - mPipBoundsState.getBounds().top; + boolean hasUserInteracted = (mPipBoundsState.hasUserMovedPip() + || mPipBoundsState.hasUserResizedPip()); + + if (!imeVisible && !hasUserInteracted) { + delta = mPipBoundsAlgorithm.getEntryDestinationBounds().top + - mPipBoundsState.getBounds().top; + } + + if ((imeVisible && delta < 0) || (!imeVisible && !hasUserInteracted)) { + // The policy is to ignore an IME disappearing if user has interacted with PiP. + // Otherwise, only offset due to an appearing IME if PiP occludes it. + mMotionHelper.animateToOffset(mPipBoundsState.getBounds(), delta); + } + }); } void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) { mIsShelfShowing = shelfVisible; mShelfHeight = shelfHeight; + + // We need to remove the callback even if the shelf is visible, in case it the delayed + // callback hasn't been executed yet to avoid the wrong final state. + mMainExecutor.removeCallbacks(mMoveOnShelVisibilityChanged); + if (shelfVisible) { + mMoveOnShelVisibilityChanged.run(); + } else { + // Postpone moving in response to hide of Launcher in case there's another change + mMainExecutor.executeDelayed(mMoveOnShelVisibilityChanged, PIP_KEEP_CLEAR_AREAS_DELAY); + } } /** @@ -438,7 +489,6 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha mPipBoundsState.setNormalMovementBounds(normalMovementBounds); mPipBoundsState.setExpandedMovementBounds(expandedMovementBounds); mDisplayRotation = displayRotation; - mInsetBounds.set(insetBounds); updateMovementBounds(); mMovementBoundsExtraOffsets = extraOffset; @@ -713,15 +763,13 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha } } - private void animateToMaximizedState(Runnable callback) { - Rect maxMovementBounds = new Rect(); + private void animateToMaximizedState() { Rect maxBounds = new Rect(0, 0, mPipBoundsState.getMaxSize().x, mPipBoundsState.getMaxSize().y); - mPipBoundsAlgorithm.getMovementBounds(maxBounds, mInsetBounds, maxMovementBounds, - mIsImeShowing ? mImeHeight : 0); + mSavedSnapFraction = mMotionHelper.animateToExpandedState(maxBounds, - mPipBoundsState.getMovementBounds(), maxMovementBounds, - callback); + getMovementBounds(mPipBoundsState.getBounds()), + getMovementBounds(maxBounds), null /* callback */); } private void animateToNormalSize(Runnable callback) { @@ -729,22 +777,20 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); final Size minMenuSize = mMenuController.getEstimatedMinMenuSize(); - final Rect normalBounds = mPipBoundsState.getNormalBounds(); - final Rect destBounds = mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu(normalBounds, - minMenuSize); - Rect restoredMovementBounds = new Rect(); - mPipBoundsAlgorithm.getMovementBounds(destBounds, - mInsetBounds, restoredMovementBounds, mIsImeShowing ? mImeHeight : 0); - mSavedSnapFraction = mMotionHelper.animateToExpandedState(destBounds, - mPipBoundsState.getMovementBounds(), restoredMovementBounds, callback); + final Size defaultSize = mSizeSpecSource.getDefaultSize(mPipBoundsState.getAspectRatio()); + final Rect normalBounds = new Rect(0, 0, defaultSize.getWidth(), defaultSize.getHeight()); + final Rect adjustedNormalBounds = mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu( + normalBounds, minMenuSize); + + mSavedSnapFraction = mMotionHelper.animateToExpandedState(adjustedNormalBounds, + getMovementBounds(mPipBoundsState.getBounds()), + getMovementBounds(adjustedNormalBounds), callback /* callback */); } private void animateToUnexpandedState(Rect restoreBounds) { - Rect restoredMovementBounds = new Rect(); - mPipBoundsAlgorithm.getMovementBounds(restoreBounds, - mInsetBounds, restoredMovementBounds, mIsImeShowing ? mImeHeight : 0); mMotionHelper.animateToUnexpandedState(restoreBounds, mSavedSnapFraction, - restoredMovementBounds, mPipBoundsState.getMovementBounds(), false /* immediate */); + getMovementBounds(restoreBounds), + getMovementBounds(mPipBoundsState.getBounds()), false /* immediate */); mSavedSnapFraction = -1f; } @@ -752,10 +798,13 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha final Rect pipBounds = mPipBoundsState.getBounds(); final boolean onLeftEdge = pipBounds.left < mPipBoundsState.getDisplayBounds().left; final Rect unStashedBounds = new Rect(0, pipBounds.top, 0, pipBounds.bottom); - unStashedBounds.left = onLeftEdge ? mInsetBounds.left - : mInsetBounds.right - pipBounds.width(); - unStashedBounds.right = onLeftEdge ? mInsetBounds.left + pipBounds.width() - : mInsetBounds.right; + + Rect insetBounds = new Rect(); + mPipBoundsAlgorithm.getInsetBounds(insetBounds); + unStashedBounds.left = onLeftEdge ? insetBounds.left + : insetBounds.right - pipBounds.width(); + unStashedBounds.right = onLeftEdge ? insetBounds.left + pipBounds.width() + : insetBounds.right; mMotionHelper.animateToUnStashedBounds(unStashedBounds); } @@ -903,8 +952,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha // Reset the touch state on up before the fling settles mTouchState.reset(); if (mEnableStash && shouldStash(vel, getPossiblyMotionBounds())) { - // mMotionHelper.stashToEdge(vel.x, vel.y, - // this::stashEndAction /* endAction */); + mMotionHelper.stashToEdge(vel.x, vel.y, null /* endAction */); } else { if (mPipBoundsState.isStashed()) { // Reset stashed state if previously stashed @@ -919,10 +967,6 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha && mMenuState != MENU_STATE_FULL) { // If using pinch to zoom, double-tap functions as resizing between max/min size if (mPipResizeGestureHandler.isUsingPinchToZoom()) { - final boolean toExpand = mPipBoundsState.getBounds().width() - < mPipBoundsState.getMaxSize().x - && mPipBoundsState.getBounds().height() - < mPipBoundsState.getMaxSize().y; if (mMenuController.isMenuVisible()) { mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */); } @@ -934,7 +978,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha // actually toggle to the size chosen if (nextSize == PipDoubleTapHelper.SIZE_SPEC_MAX) { mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); - animateToMaximizedState(null); + animateToMaximizedState(); } else if (nextSize == PipDoubleTapHelper.SIZE_SPEC_DEFAULT) { mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); animateToNormalSize(null); @@ -1037,15 +1081,19 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha * Updates the current movement bounds based on whether the menu is currently visible and * resized. */ - private void updateMovementBounds() { + void updateMovementBounds() { + Rect insetBounds = new Rect(); + mPipBoundsAlgorithm.getInsetBounds(insetBounds); mPipBoundsAlgorithm.getMovementBounds(mPipBoundsState.getBounds(), - mInsetBounds, mPipBoundsState.getMovementBounds(), mIsImeShowing ? mImeHeight : 0); + insetBounds, mPipBoundsState.getMovementBounds(), mIsImeShowing ? mImeHeight : 0); mMotionHelper.onMovementBoundsChanged(); } private Rect getMovementBounds(Rect curBounds) { Rect movementBounds = new Rect(); - mPipBoundsAlgorithm.getMovementBounds(curBounds, mInsetBounds, + Rect insetBounds = new Rect(); + mPipBoundsAlgorithm.getInsetBounds(insetBounds); + mPipBoundsAlgorithm.getMovementBounds(curBounds, insetBounds, movementBounds, mIsImeShowing ? mImeHeight : 0); return movementBounds; } @@ -1090,7 +1138,9 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha switch (newState) { case PipTransitionState.ENTERED_PIP: onActivityPinned(); + updateMovementBounds(); mTouchState.setAllowInputEvents(true); + mTouchState.reset(); break; case PipTransitionState.EXITED_PIP: mTouchState.setAllowInputEvents(false); @@ -1101,6 +1151,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha break; case PipTransitionState.CHANGED_PIP_BOUNDS: mTouchState.setAllowInputEvents(true); + mTouchState.reset(); break; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchState.java index d093f1e5ccc1..bb8d4ee9c80f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchState.java @@ -23,7 +23,7 @@ import android.view.VelocityTracker; import android.view.ViewConfiguration; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.protolog.ShellProtoLogGroup; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 57dc5f92b2b6..f93233ec7461 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -36,6 +36,7 @@ import android.content.Context; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; +import android.view.Surface; import android.view.SurfaceControl; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; @@ -50,9 +51,10 @@ import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUtils; -import com.android.wm.shell.pip.PipContentOverlay; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; +import com.android.wm.shell.pip2.animation.PipEnterExitAnimator; +import com.android.wm.shell.shared.pip.PipContentOverlay; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; @@ -71,6 +73,9 @@ public class PipTransition extends PipTransitionController implements static final String PIP_START_TX = "pip_start_tx"; static final String PIP_FINISH_TX = "pip_finish_tx"; static final String PIP_DESTINATION_BOUNDS = "pip_dest_bounds"; + static final String ANIMATING_BOUNDS_CHANGE_DURATION = + "animating_bounds_change_duration"; + static final int BOUNDS_CHANGE_JUMPCUT_DURATION = 0; /** * The fixed start delay in ms when fading out the content overlay from bounds animation. @@ -83,11 +88,12 @@ public class PipTransition extends PipTransitionController implements // private final Context mContext; + private final PipTaskListener mPipTaskListener; private final PipScheduler mPipScheduler; private final PipTransitionState mPipTransitionState; // - // Transition tokens + // Transition caches // @Nullable @@ -96,16 +102,14 @@ public class PipTransition extends PipTransitionController implements private IBinder mExitViaExpandTransition; @Nullable private IBinder mResizeTransition; + private int mBoundsChangeDuration = BOUNDS_CHANGE_JUMPCUT_DURATION; + // // Internal state and relevant cached info // @Nullable - private WindowContainerToken mPipTaskToken; - @Nullable - private SurfaceControl mPipLeash; - @Nullable private Transitions.TransitionFinishCallback mFinishCallback; public PipTransition( @@ -116,12 +120,15 @@ public class PipTransition extends PipTransitionController implements PipBoundsState pipBoundsState, PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm, + PipTaskListener pipTaskListener, PipScheduler pipScheduler, - PipTransitionState pipTransitionState) { + PipTransitionState pipTransitionState, + PipUiStateChangeController pipUiStateChangeController) { super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController, pipBoundsAlgorithm); mContext = context; + mPipTaskListener = pipTaskListener; mPipScheduler = pipScheduler; mPipScheduler.setPipTransitionController(this); mPipTransitionState = pipTransitionState; @@ -135,6 +142,11 @@ public class PipTransition extends PipTransitionController implements } } + @Override + protected boolean isInSwipePipToHomeTransition() { + return mPipTransitionState.isInSwipePipToHomeTransition(); + } + // // Transition collection stage lifecycle hooks // @@ -152,11 +164,12 @@ public class PipTransition extends PipTransitionController implements } @Override - public void startResizeTransition(WindowContainerTransaction wct) { + public void startResizeTransition(WindowContainerTransaction wct, int duration) { if (wct == null) { return; } mResizeTransition = mTransitions.startTransition(TRANSIT_RESIZE_PIP, wct, this); + mBoundsChangeDuration = duration; } @Nullable @@ -272,6 +285,10 @@ public class PipTransition extends PipTransitionController implements extra.putParcelable(PIP_START_TX, startTransaction); extra.putParcelable(PIP_FINISH_TX, finishTransaction); extra.putParcelable(PIP_DESTINATION_BOUNDS, pipChange.getEndAbsBounds()); + if (mBoundsChangeDuration > BOUNDS_CHANGE_JUMPCUT_DURATION) { + extra.putInt(ANIMATING_BOUNDS_CHANGE_DURATION, mBoundsChangeDuration); + mBoundsChangeDuration = BOUNDS_CHANGE_JUMPCUT_DURATION; + } mFinishCallback = finishCallback; mPipTransitionState.setState(PipTransitionState.CHANGING_PIP_BOUNDS, extra); @@ -368,12 +385,38 @@ public class PipTransition extends PipTransitionController implements if (pipChange == null) { return false; } - // cache the PiP task token and leash + WindowContainerToken pipTaskToken = pipChange.getContainer(); + if (pipTaskToken == null) { + return false; + } - startTransaction.apply(); - // TODO: b/275910498 Use a new implementation of the PiP animator here. - finishCallback.onTransitionFinished(null); + WindowContainerTransaction finishWct = new WindowContainerTransaction(); + SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + + Rect startBounds = pipChange.getStartAbsBounds(); + Rect endBounds = pipChange.getEndAbsBounds(); + SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash; + Preconditions.checkNotNull(pipLeash, "Leash is null for bounds transition."); + + Rect sourceRectHint = null; + if (pipChange.getTaskInfo() != null + && pipChange.getTaskInfo().pictureInPictureParams != null) { + sourceRectHint = pipChange.getTaskInfo().pictureInPictureParams.getSourceRectHint(); + } + + PipEnterExitAnimator animator = new PipEnterExitAnimator(mContext, pipLeash, + startTransaction, finishTransaction, startBounds, startBounds, endBounds, + sourceRectHint, PipEnterExitAnimator.BOUNDS_ENTER, Surface.ROTATION_0); + + tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(), + this::onClientDrawAtTransitionEnd); + finishWct.setBoundsChangeTransaction(pipTaskToken, tx); + + animator.setAnimationEndCallback(() -> + finishCallback.onTransitionFinished(finishWct)); + + animator.start(); return true; } @@ -411,10 +454,60 @@ public class PipTransition extends PipTransitionController implements @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { - startTransaction.apply(); - // TODO: b/275910498 Use a new implementation of the PiP animator here. - finishCallback.onTransitionFinished(null); - mPipTransitionState.setState(PipTransitionState.EXITED_PIP); + WindowContainerToken pipToken = mPipTransitionState.mPipTaskToken; + + TransitionInfo.Change pipChange = getChangeByToken(info, pipToken); + if (pipChange == null) { + // pipChange is null, check to see if we've reparented the PIP activity for + // the multi activity case. If so we should use the activity leash instead + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() == null + && change.getLastParent() != null + && change.getLastParent().equals(pipToken)) { + pipChange = change; + break; + } + } + + // failsafe + if (pipChange == null) { + return false; + } + } + + // for multi activity, we need to manually set the leash layer + if (pipChange.getTaskInfo() == null) { + TransitionInfo.Change parent = getChangeByToken(info, pipChange.getParent()); + if (parent != null) { + startTransaction.setLayer(parent.getLeash(), Integer.MAX_VALUE - 1); + } + } + + Rect startBounds = pipChange.getStartAbsBounds(); + Rect endBounds = pipChange.getEndAbsBounds(); + SurfaceControl pipLeash = pipChange.getLeash(); + Preconditions.checkNotNull(pipLeash, "Leash is null for exit transition."); + + Rect sourceRectHint = null; + if (pipChange.getTaskInfo() != null + && pipChange.getTaskInfo().pictureInPictureParams != null) { + // single activity + sourceRectHint = pipChange.getTaskInfo().pictureInPictureParams.getSourceRectHint(); + } else if (mPipTaskListener.getPictureInPictureParams().hasSourceBoundsHint()) { + // multi activity + sourceRectHint = mPipTaskListener.getPictureInPictureParams().getSourceRectHint(); + } + + PipEnterExitAnimator animator = new PipEnterExitAnimator(mContext, pipLeash, + startTransaction, finishTransaction, endBounds, startBounds, endBounds, + sourceRectHint, PipEnterExitAnimator.BOUNDS_EXIT, Surface.ROTATION_0); + + animator.setAnimationEndCallback(() -> { + mPipTransitionState.setState(PipTransitionState.EXITED_PIP); + finishCallback.onTransitionFinished(null); + }); + + animator.start(); return true; } @@ -443,11 +536,24 @@ public class PipTransition extends PipTransitionController implements return null; } + @Nullable + private TransitionInfo.Change getChangeByToken(TransitionInfo info, + WindowContainerToken token) { + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() != null + && change.getTaskInfo().getToken().equals(token)) { + return change; + } + } + return null; + } + private WindowContainerTransaction getEnterPipTransaction(@NonNull IBinder transition, @NonNull TransitionRequestInfo request) { // cache the original task token to check for multi-activity case later final ActivityManager.RunningTaskInfo pipTask = request.getPipTask(); PictureInPictureParams pipParams = pipTask.pictureInPictureParams; + mPipTaskListener.setPictureInPictureParams(pipParams); mPipBoundsState.setBoundsStateForEntry(pipTask.topActivity, pipTask.topActivityInfo, pipParams, mPipBoundsAlgorithm); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java index 9d599caf13dd..a132796f4a84 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java @@ -29,6 +29,7 @@ import androidx.annotation.Nullable; import com.android.internal.util.Preconditions; import com.android.wm.shell.shared.annotations.ShellMainThread; +import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -62,6 +63,8 @@ import java.util.List; * and throw an <code>IllegalStateException</code> otherwise.</p> */ public class PipTransitionState { + private static final String TAG = PipTransitionState.class.getSimpleName(); + public static final int UNDEFINED = 0; // State for Launcher animating the swipe PiP to home animation. @@ -146,6 +149,12 @@ public class PipTransitionState { @Nullable private SurfaceControl mSwipePipToHomeOverlay; + // + // Scheduling-related state + // + @Nullable + private Runnable mOnIdlePipTransitionStateRunnable; + /** * An interface to track state updates as we progress through PiP transitions. */ @@ -190,9 +199,12 @@ public class PipTransitionState { "No extra bundle for " + stateToString(state) + " state."); } if (mState != state) { - dispatchPipTransitionStateChanged(mState, state, extra); + final int prevState = mState; mState = state; + dispatchPipTransitionStateChanged(prevState, mState, extra); } + + maybeRunOnIdlePipTransitionStateCallback(); } /** @@ -227,6 +239,29 @@ public class PipTransitionState { } /** + * Schedule a callback to run when in a valid idle PiP state. + * + * <p>We only allow for one callback to be scheduled to avoid cases with multiple transitions + * being scheduled. For instance, if user double taps and IME shows, this would + * schedule a bounds change transition for IME appearing. But if some other transition would + * want to animate PiP before the scheduled callback executes, we would rather want to replace + * the existing callback with a new one, to avoid multiple animations + * as soon as we are idle.</p> + */ + public void setOnIdlePipTransitionStateRunnable( + @Nullable Runnable onIdlePipTransitionStateRunnable) { + mOnIdlePipTransitionStateRunnable = onIdlePipTransitionStateRunnable; + maybeRunOnIdlePipTransitionStateCallback(); + } + + private void maybeRunOnIdlePipTransitionStateCallback() { + if (mOnIdlePipTransitionStateRunnable != null && isPipStateIdle()) { + mOnIdlePipTransitionStateRunnable.run(); + mOnIdlePipTransitionStateRunnable = null; + } + } + + /** * Adds a {@link PipTransitionStateChangedListener} for future PiP transition state updates. */ public void addPipTransitionStateChangedListener(PipTransitionStateChangedListener listener) { @@ -314,9 +349,21 @@ public class PipTransitionState { throw new IllegalStateException("Unknown state: " + state); } + public boolean isPipStateIdle() { + // This needs to be a valid in-PiP state that isn't a transient state. + return mState == ENTERED_PIP || mState == CHANGED_PIP_BOUNDS; + } + @Override public String toString() { return String.format("PipTransitionState(mState=%s, mInSwipePipToHomeTransition=%b)", stateToString(mState), mInSwipePipToHomeTransition); } + + /** Dumps internal state. */ + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mState=" + stateToString(mState)); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipUiStateChangeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipUiStateChangeController.java new file mode 100644 index 000000000000..224016e6c9d4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipUiStateChangeController.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 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.pip2.phone; + +import android.app.ActivityTaskManager; +import android.app.Flags; +import android.app.PictureInPictureUiState; +import android.os.Bundle; +import android.os.RemoteException; + +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.ProtoLog; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.util.function.Consumer; + +/** + * Controller class manages the {@link android.app.PictureInPictureUiState} callbacks sent to app. + */ +public class PipUiStateChangeController implements + PipTransitionState.PipTransitionStateChangedListener { + + private final PipTransitionState mPipTransitionState; + + private Consumer<PictureInPictureUiState> mPictureInPictureUiStateConsumer; + + public PipUiStateChangeController(PipTransitionState pipTransitionState) { + mPipTransitionState = pipTransitionState; + mPipTransitionState.addPipTransitionStateChangedListener(this); + mPictureInPictureUiStateConsumer = pictureInPictureUiState -> { + try { + ActivityTaskManager.getService().onPictureInPictureUiStateChanged( + pictureInPictureUiState); + } catch (RemoteException | IllegalStateException e) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "Failed to send PictureInPictureUiState."); + } + }; + } + + @Override + public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, + @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) { + if (newState == PipTransitionState.SWIPING_TO_PIP) { + onIsTransitioningToPipUiStateChange(true /* isTransitioningToPip */); + } else if (newState == PipTransitionState.ENTERING_PIP + && !mPipTransitionState.isInSwipePipToHomeTransition()) { + onIsTransitioningToPipUiStateChange(true /* isTransitioningToPip */); + } else if (newState == PipTransitionState.ENTERED_PIP) { + onIsTransitioningToPipUiStateChange(false /* isTransitioningToPip */); + } + } + + @VisibleForTesting + void setPictureInPictureUiStateConsumer(Consumer<PictureInPictureUiState> consumer) { + mPictureInPictureUiStateConsumer = consumer; + } + + private void onIsTransitioningToPipUiStateChange(boolean isTransitioningToPip) { + if (Flags.enablePipUiStateCallbackOnEntering() + && mPictureInPictureUiStateConsumer != null) { + mPictureInPictureUiStateConsumer.accept(new PictureInPictureUiState.Builder() + .setTransitioningToPip(isTransitioningToPip) + .build()); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java index 497c3f704c82..f739d65e63c3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java @@ -61,6 +61,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { Consts.TAG_WM_SHELL), WM_SHELL_BUBBLES(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, "Bubbles"), + WM_SHELL_COMPAT_UI(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM_COMPAT_UI), TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest"); private final boolean mEnabled; @@ -128,6 +130,7 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { private static final String TAG_WM_STARTING_WINDOW = "ShellStartingWindow"; private static final String TAG_WM_SPLIT_SCREEN = "ShellSplitScreen"; private static final String TAG_WM_DESKTOP_MODE = "ShellDesktopMode"; + private static final String TAG_WM_COMPAT_UI = "CompatUi"; private static final boolean ENABLE_DEBUG = true; private static final boolean ENABLE_LOG_TO_PROTO_DEBUG = true; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl index 4048c5b8feab..799028a5507a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl @@ -21,10 +21,10 @@ import android.app.IApplicationThread; import android.app.PendingIntent; import android.content.Intent; import android.os.Bundle; -import android.view.IRecentsAnimationRunner; +import com.android.wm.shell.recents.IRecentsAnimationRunner; import com.android.wm.shell.recents.IRecentTasksListener; -import com.android.wm.shell.util.GroupedRecentTaskInfo; +import com.android.wm.shell.shared.GroupedRecentTaskInfo; /** * Interface that is exposed to remote callers to fetch recent tasks. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl new file mode 100644 index 000000000000..964e5fd62a5f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2018 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.recents; + +import android.graphics.GraphicBuffer; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.window.PictureInPictureSurfaceTransaction; +import android.window.TaskSnapshot; +import android.window.WindowAnimationState; + +import com.android.internal.os.IResultReceiver; + +/** + * Passed to the {@link IRecentsAnimationRunner} in order for the runner to control to let the + * runner control certain aspects of the recents animation, and to notify window manager when the + * animation has completed. + * + * {@hide} + */ +interface IRecentsAnimationController { + + /** + * Takes a screenshot of the task associated with the given {@param taskId}. Only valid for the + * current set of task ids provided to the handler. + */ + TaskSnapshot screenshotTask(int taskId); + + /** + * Sets the final surface transaction on a Task. This is used by Launcher to notify the system + * that animating Activity to PiP has completed and the associated task surface should be + * updated accordingly. This should be called before `finish` + * @param taskId for which the leash should be updated + * @param finishTransaction leash operations for the final transform. + * @param overlay the surface control for an overlay being shown above the pip (can be null) + */ + void setFinishTaskTransaction(int taskId, + in PictureInPictureSurfaceTransaction finishTransaction, in SurfaceControl overlay); + + /** + * Notifies to the system that the animation into Recents should end, and all leashes associated + * with remote animation targets should be relinquished. If {@param moveHomeToTop} is true, then + * the home activity should be moved to the top. Otherwise, the home activity is hidden and the + * user is returned to the app. + * @param sendUserLeaveHint If set to true, {@link Activity#onUserLeaving} will be sent to the + * top resumed app, false otherwise. + */ + void finish(boolean moveHomeToTop, boolean sendUserLeaveHint, in IResultReceiver finishCb); + + /** + * Called by the handler to indicate that the recents animation input consumer should be + * enabled. This is currently used to work around an issue where registering an input consumer + * mid-animation causes the existing motion event chain to be canceled. Instead, the caller + * may register the recents animation input consumer prior to starting the recents animation + * and then enable it mid-animation to start receiving touch events. + */ + void setInputConsumerEnabled(boolean enabled); + + /** + * Sets a state for controller to decide which surface is the destination when the recents + * animation is cancelled through fail safe mechanism. + */ + void setWillFinishToHome(boolean willFinishToHome); + + /** + * Detach navigation bar from app. + * + * The system reparents the leash of navigation bar to the app when the recents animation starts + * and Launcher should call this method to let system restore the navigation bar to its + * original position when the quick switch gesture is finished and will run the fade-in + * animation If {@param moveHomeToTop} is {@code true}. Otherwise, restore the navigtation bar + * without animation. + * + * @param moveHomeToTop if {@code true}, the home activity should be moved to the top. + * Otherwise, the home activity is hidden and the user is returned to the + * app. + */ + void detachNavigationBarFromApp(boolean moveHomeToTop); + + /** + * Hand off the ongoing animation of a set of remote targets, to be run by another handler using + * the given starting parameters. + * + * Once the handoff is complete, operations on the old leashes for the given targets as well as + * callbacks will become no-ops. + * + * The number of targets MUST match the number of states, and each state MUST match the target + * at the same index. + */ + oneway void handOffAnimation(in RemoteAnimationTarget[] targets, + in WindowAnimationState[] states); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl new file mode 100644 index 000000000000..32c79a2d02de --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2018 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.recents; + +import android.graphics.Rect; +import android.view.RemoteAnimationTarget; +import android.window.TaskSnapshot; +import android.os.Bundle; + +import com.android.wm.shell.recents.IRecentsAnimationController; + +/** + * Interface that is used to callback from window manager to the process that runs a recents + * animation to start or cancel it. + * + * {@hide} + */ +oneway interface IRecentsAnimationRunner { + + /** + * Called when the system needs to cancel the current animation. This can be due to the + * wallpaper not drawing in time, or the handler not finishing the animation within a predefined + * amount of time. + * + * @param taskIds Indicates tasks with cancelling snapshot. + * @param taskSnapshots If the snapshots is null, the animation will be cancelled and the leash + * will be inactive immediately. Otherwise, the contents of the tasks will + * be replaced with {@param taskSnapshots}, such that the runner's leash is + * still active. As soon as the runner doesn't need the leash anymore, it + * must call {@link IRecentsAnimationController#cleanupScreenshot). + * + * @see {@link RecentsAnimationController#cleanupScreenshot} + */ + void onAnimationCanceled(in @nullable int[] taskIds, + in @nullable TaskSnapshot[] taskSnapshots) = 1; + + /** + * Called when the system is ready for the handler to start animating all the visible tasks. + * + * @param homeContentInsets The current home app content insets + * @param minimizedHomeBounds Specifies the bounds of the minimized home app, will be + * {@code null} if the device is not currently in split screen + */ + void onAnimationStart(in IRecentsAnimationController controller, + in RemoteAnimationTarget[] apps, in RemoteAnimationTarget[] wallpapers, + in Rect homeContentInsets, in Rect minimizedHomeBounds, in Bundle extras) = 2; + + /** + * Called when the task of an activity that has been started while the recents animation + * was running becomes ready for control. + */ + void onTasksAppeared(in RemoteAnimationTarget[] app) = 3; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java index 77b8663861ab..8c5d1e7e069d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java @@ -19,8 +19,8 @@ package com.android.wm.shell.recents; import android.annotation.Nullable; import android.graphics.Color; +import com.android.wm.shell.shared.GroupedRecentTaskInfo; import com.android.wm.shell.shared.annotations.ExternalThread; -import com.android.wm.shell.util.GroupedRecentTaskInfo; import java.util.List; import java.util.concurrent.Executor; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index 03c8cf8cc795..03ff1aac794c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java @@ -19,13 +19,14 @@ package com.android.wm.shell.recents; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.pm.PackageManager.FEATURE_PC; -import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps; -import static com.android.window.flags.Flags.enableTaskStackObserverInShell; -import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS; +import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS; +import android.Manifest; +import android.annotation.RequiresPermission; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.IApplicationThread; +import android.app.KeyguardManager; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; @@ -36,14 +37,15 @@ import android.os.RemoteException; import android.util.Slog; import android.util.SparseArray; import android.util.SparseIntArray; -import android.view.IRecentsAnimationRunner; +import android.window.WindowContainerToken; +import android.window.flags.DesktopModeFlags; import androidx.annotation.BinderThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; @@ -52,22 +54,24 @@ import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import com.android.wm.shell.shared.DesktopModeStatus; +import com.android.wm.shell.shared.GroupedRecentTaskInfo; import com.android.wm.shell.shared.annotations.ExternalThread; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.shared.split.SplitBounds; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; -import com.android.wm.shell.util.GroupedRecentTaskInfo; -import com.android.wm.shell.util.SplitBounds; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -156,6 +160,7 @@ public class RecentTasksController implements TaskStackListenerCallback, return new IRecentTasksImpl(this); } + @RequiresPermission(Manifest.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE) private void onInit() { mShellController.addExternalInterface(KEY_EXTRA_SHELL_RECENT_TASKS, this::createExternalInterface, this); @@ -166,6 +171,8 @@ public class RecentTasksController implements TaskStackListenerCallback, mTaskStackTransitionObserver.addTaskStackTransitionObserverListener(this, mMainExecutor); } + mContext.getSystemService(KeyguardManager.class).addKeyguardLockedStateListener( + mMainExecutor, isKeyguardLocked -> notifyRecentTasksChanged()); } void setTransitionHandler(RecentsTransitionHandler handler) { @@ -348,7 +355,7 @@ public class RecentTasksController implements TaskStackListenerCallback, private void notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) { if (mListener == null - || !enableTaskStackObserverInShell() + || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue() || taskInfo.realActivity == null) { return; } @@ -362,7 +369,7 @@ public class RecentTasksController implements TaskStackListenerCallback, private boolean shouldEnableRunningTasksForDesktopMode() { return mPcFeatureEnabled || (DesktopModeStatus.canEnterDesktopMode(mContext) - && enableDesktopWindowingTaskbarRunningApps()); + && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS.isTrue()); } @VisibleForTesting @@ -394,6 +401,7 @@ public class RecentTasksController implements TaskStackListenerCallback, } ArrayList<ActivityManager.RecentTaskInfo> freeformTasks = new ArrayList<>(); + Set<Integer> minimizedFreeformTasks = new HashSet<>(); int mostRecentFreeformTaskIndex = Integer.MAX_VALUE; @@ -409,15 +417,14 @@ public class RecentTasksController implements TaskStackListenerCallback, if (DesktopModeStatus.canEnterDesktopMode(mContext) && mDesktopModeTaskRepository.isPresent() && mDesktopModeTaskRepository.get().isActiveTask(taskInfo.taskId)) { - if (mDesktopModeTaskRepository.get().isMinimizedTask(taskInfo.taskId)) { - // Minimized freeform tasks should not be shown at all. - continue; - } // Freeform tasks will be added as a separate entry if (mostRecentFreeformTaskIndex == Integer.MAX_VALUE) { mostRecentFreeformTaskIndex = recentTasks.size(); } freeformTasks.add(taskInfo); + if (mDesktopModeTaskRepository.get().isMinimizedTask(taskInfo.taskId)) { + minimizedFreeformTasks.add(taskInfo.taskId); + } continue; } @@ -435,8 +442,10 @@ public class RecentTasksController implements TaskStackListenerCallback, // Add a special entry for freeform tasks if (!freeformTasks.isEmpty()) { - recentTasks.add(mostRecentFreeformTaskIndex, GroupedRecentTaskInfo.forFreeformTasks( - freeformTasks.toArray(new ActivityManager.RecentTaskInfo[0]))); + recentTasks.add(mostRecentFreeformTaskIndex, + GroupedRecentTaskInfo.forFreeformTasks( + freeformTasks.toArray(new ActivityManager.RecentTaskInfo[0]), + minimizedFreeformTasks)); } return recentTasks; @@ -453,11 +462,31 @@ public class RecentTasksController implements TaskStackListenerCallback, } /** - * Find the background task that match the given component. + * Returns the top running leaf task ignoring {@param ignoreTaskToken} if it is specified. + * NOTE: This path currently makes assumptions that ignoreTaskToken is for the top task. + */ + @Nullable + public ActivityManager.RunningTaskInfo getTopRunningTask( + @Nullable WindowContainerToken ignoreTaskToken) { + List<ActivityManager.RunningTaskInfo> tasks = mActivityTaskManager.getTasks(2, + false /* filterOnlyVisibleRecents */); + for (int i = 0; i < tasks.size(); i++) { + final ActivityManager.RunningTaskInfo task = tasks.get(i); + if (task.token.equals(ignoreTaskToken)) { + continue; + } + return task; + } + return null; + } + + /** + * Find the background task that match the given component. Ignores tasks match + * {@param ignoreTaskToken} if it is non-null. */ @Nullable public ActivityManager.RecentTaskInfo findTaskInBackground(ComponentName componentName, - int userId) { + int userId, @Nullable WindowContainerToken ignoreTaskToken) { if (componentName == null) { return null; } @@ -469,6 +498,9 @@ public class RecentTasksController implements TaskStackListenerCallback, if (task.isVisible) { continue; } + if (task.token.equals(ignoreTaskToken)) { + continue; + } if (componentName.equals(task.baseIntent.getComponent()) && userId == task.userId) { return task; } @@ -636,7 +668,7 @@ public class RecentTasksController implements TaskStackListenerCallback, @Override public ActivityManager.RunningTaskInfo[] getRunningTasks(int maxNum) { final ActivityManager.RunningTaskInfo[][] tasks = - new ActivityManager.RunningTaskInfo[][] {null}; + new ActivityManager.RunningTaskInfo[][]{null}; executeRemoteCallWithTaskPermission(mController, "getRunningTasks", (controller) -> tasks[0] = ActivityTaskManager.getInstance().getTasks(maxNum) .toArray(new ActivityManager.RunningTaskInfo[0]), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 3a266d9bb3ef..8077aeebf27f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -19,16 +19,21 @@ package com.android.wm.shell.recents; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS; import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED; +import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; -import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION; -import static com.android.wm.shell.util.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS; +import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION; +import static com.android.wm.shell.shared.split.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS; import android.annotation.Nullable; import android.annotation.SuppressLint; @@ -39,6 +44,7 @@ import android.app.PendingIntent; import android.content.Intent; import android.graphics.Color; import android.graphics.Rect; +import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; @@ -47,8 +53,6 @@ import android.util.IntArray; import android.util.Pair; import android.util.Slog; import android.view.Display; -import android.view.IRecentsAnimationController; -import android.view.IRecentsAnimationRunner; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.window.PictureInPictureSurfaceTransaction; @@ -63,7 +67,8 @@ import androidx.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.IResultReceiver; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; +import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -79,10 +84,15 @@ import java.util.function.Consumer; * Handles the Recents (overview) animation. Only one of these can run at a time. A recents * transition must be created via {@link #startRecentsTransition}. Anything else will be ignored. */ -public class RecentsTransitionHandler implements Transitions.TransitionHandler { +public class RecentsTransitionHandler implements Transitions.TransitionHandler, + Transitions.TransitionObserver { private static final String TAG = "RecentsTransitionHandler"; + // A placeholder for a synthetic transition that isn't backed by a true system transition + public static final IBinder SYNTHETIC_TRANSITION = new Binder(); + private final Transitions mTransitions; + private final ShellTaskOrganizer mShellTaskOrganizer; private final ShellExecutor mExecutor; @Nullable private final RecentTasksController mRecentTasksController; @@ -99,19 +109,26 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { private final HomeTransitionObserver mHomeTransitionObserver; private @Nullable Color mBackgroundColor; - public RecentsTransitionHandler(ShellInit shellInit, Transitions transitions, + public RecentsTransitionHandler( + @NonNull ShellInit shellInit, + @NonNull ShellTaskOrganizer shellTaskOrganizer, + @NonNull Transitions transitions, @Nullable RecentTasksController recentTasksController, - HomeTransitionObserver homeTransitionObserver) { + @NonNull HomeTransitionObserver homeTransitionObserver) { + mShellTaskOrganizer = shellTaskOrganizer; mTransitions = transitions; mExecutor = transitions.getMainExecutor(); mRecentTasksController = recentTasksController; mHomeTransitionObserver = homeTransitionObserver; if (!Transitions.ENABLE_SHELL_TRANSITIONS) return; if (recentTasksController == null) return; - shellInit.addInitCallback(() -> { - recentTasksController.setTransitionHandler(this); - transitions.addHandler(this); - }, this); + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + mRecentTasksController.setTransitionHandler(this); + mTransitions.addHandler(this); + mTransitions.registerObserver(this); } /** Register a mixer handler. {@see RecentsMixedHandler}*/ @@ -138,17 +155,59 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { mBackgroundColor = color; } + /** + * Starts a new real/synthetic recents transition. + */ @VisibleForTesting public IBinder startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options, IApplicationThread appThread, IRecentsAnimationRunner listener) { + // only care about latest one. + mAnimApp = appThread; + + // TODO(b/366021931): Formalize this later + final boolean isSyntheticRequest = options.containsKey("is_synthetic_recents_transition"); + if (isSyntheticRequest) { + return startSyntheticRecentsTransition(listener); + } else { + return startRealRecentsTransition(intent, fillIn, options, listener); + } + } + + /** + * Starts a synthetic recents transition that is not backed by a real WM transition. + */ + private IBinder startSyntheticRecentsTransition(@NonNull IRecentsAnimationRunner listener) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "RecentsTransitionHandler.startRecentsTransition(synthetic)"); + final RecentsController lastController = getLastController(); + if (lastController != null) { + lastController.cancel(lastController.isSyntheticTransition() + ? "existing_running_synthetic_transition" + : "existing_running_transition"); + return null; + } + + // Create a new synthetic transition and start it immediately + final RecentsController controller = new RecentsController(listener); + controller.startSyntheticTransition(); + mControllers.add(controller); + return SYNTHETIC_TRANSITION; + } + + /** + * Starts a real WM-backed recents transition. + */ + private IBinder startRealRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options, + IRecentsAnimationRunner listener) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "RecentsTransitionHandler.startRecentsTransition"); - // only care about latest one. - mAnimApp = appThread; - WindowContainerTransaction wct = new WindowContainerTransaction(); + final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.sendPendingIntent(intent, fillIn, options); - final RecentsController controller = new RecentsController(listener); + + // Find the mixed handler which should handle this request (if we are in a state where a + // mixed handler is needed). This is slightly convoluted because starting the transition + // requires the handler, but the mixed handler also needs a reference to the transition. RecentsMixedHandler mixer = null; Consumer<IBinder> setTransitionForMixer = null; for (int i = 0; i < mMixers.size(); ++i) { @@ -160,12 +219,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } final IBinder transition = mTransitions.startTransition(TRANSIT_TO_FRONT, wct, mixer == null ? this : mixer); - for (int i = 0; i < mStateListeners.size(); i++) { - mStateListeners.get(i).onTransitionStarted(transition); - } if (mixer != null) { setTransitionForMixer.accept(transition); } + + final RecentsController controller = new RecentsController(listener); if (transition != null) { controller.setTransition(transition); mControllers.add(controller); @@ -187,11 +245,28 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { return null; } - private int findController(IBinder transition) { + /** + * Returns if there is currently a pending or active recents transition. + */ + @Nullable + private RecentsController getLastController() { + return !mControllers.isEmpty() ? mControllers.getLast() : null; + } + + /** + * Finds an existing controller for the provided {@param transition}, or {@code null} if none + * exists. + */ + @Nullable + @VisibleForTesting + RecentsController findController(@NonNull IBinder transition) { for (int i = mControllers.size() - 1; i >= 0; --i) { - if (mControllers.get(i).mTransition == transition) return i; + final RecentsController controller = mControllers.get(i); + if (controller.mTransition == transition) { + return controller; + } } - return -1; + return null; } @Override @@ -199,13 +274,12 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { SurfaceControl.Transaction startTransaction, SurfaceControl.Transaction finishTransaction, Transitions.TransitionFinishCallback finishCallback) { - final int controllerIdx = findController(transition); - if (controllerIdx < 0) { + final RecentsController controller = findController(transition); + if (controller == null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "RecentsTransitionHandler.startAnimation: no controller found"); return false; } - final RecentsController controller = mControllers.get(controllerIdx); final IApplicationThread animApp = mAnimApp; mAnimApp = null; if (!controller.start(info, startTransaction, finishTransaction, finishCallback)) { @@ -221,13 +295,12 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { public void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) { - final int targetIdx = findController(mergeTarget); - if (targetIdx < 0) { + final RecentsController controller = findController(mergeTarget); + if (controller == null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "RecentsTransitionHandler.mergeAnimation: no controller found"); return; } - final RecentsController controller = mControllers.get(targetIdx); controller.merge(info, t, finishCallback); } @@ -244,8 +317,21 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } } + @Override + public void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + RecentsController controller = findController(SYNTHETIC_TRANSITION); + if (controller != null) { + // Cancel the existing synthetic transition if there is one + controller.cancel("incoming_transition"); + } + } + /** There is only one of these and it gets reset on finish. */ - private class RecentsController extends IRecentsAnimationController.Stub { + @VisibleForTesting + class RecentsController extends IRecentsAnimationController.Stub { + private final int mInstanceId; private IRecentsAnimationRunner mListener; @@ -307,7 +393,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { mDeathHandler = () -> { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.DeathRecipient: binder died", mInstanceId); - finish(mWillFinishToHome, false /* leaveHint */, null /* finishCb */); + finishInner(mWillFinishToHome, false /* leaveHint */, null /* finishCb */, + "deathRecipient"); }; try { mListener.asBinder().linkToDeath(mDeathHandler, 0 /* flags */); @@ -317,6 +404,9 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } } + /** + * Sets the started transition for this instance of the recents transition. + */ void setTransition(IBinder transition) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.setTransition: id=%s", mInstanceId, transition); @@ -330,6 +420,10 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } void cancel(boolean toHome, boolean withScreenshots, String reason) { + if (cancelSyntheticTransition(reason)) { + return; + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.cancel: toHome=%b reason=%s", mInstanceId, toHome, reason); @@ -341,7 +435,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } } if (mFinishCB != null) { - finishInner(toHome, false /* userLeave */, null /* finishCb */); + finishInner(toHome, false /* userLeave */, null /* finishCb */, "cancel"); } else { cleanUp(); } @@ -436,6 +530,91 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } } + /** + * Starts a new transition that is not backed by a system transition. + */ + void startSyntheticTransition() { + mTransition = SYNTHETIC_TRANSITION; + + // TODO(b/366021931): Update mechanism for pulling the home task, for now add home as + // both opening and closing since there's some pre-existing + // dependencies on having a closing task + final ActivityManager.RunningTaskInfo homeTask = + mShellTaskOrganizer.getRunningTasks(DEFAULT_DISPLAY).stream() + .filter(task -> task.getActivityType() == ACTIVITY_TYPE_HOME) + .findFirst() + .get(); + final RemoteAnimationTarget openingTarget = TransitionUtil.newSyntheticTarget( + homeTask, mShellTaskOrganizer.getHomeTaskOverlayContainer(), TRANSIT_OPEN, + 0, true /* isTranslucent */); + final RemoteAnimationTarget closingTarget = TransitionUtil.newSyntheticTarget( + homeTask, mShellTaskOrganizer.getHomeTaskOverlayContainer(), TRANSIT_CLOSE, + 0, true /* isTranslucent */); + final ArrayList<RemoteAnimationTarget> apps = new ArrayList<>(); + apps.add(openingTarget); + apps.add(closingTarget); + try { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.start: calling onAnimationStart with %d apps", + mInstanceId, apps.size()); + mListener.onAnimationStart(this, + apps.toArray(new RemoteAnimationTarget[apps.size()]), + new RemoteAnimationTarget[0], + new Rect(0, 0, 0, 0), new Rect(), new Bundle()); + for (int i = 0; i < mStateListeners.size(); i++) { + mStateListeners.get(i).onAnimationStateChanged(true); + } + } catch (RemoteException e) { + Slog.e(TAG, "Error starting recents animation", e); + cancel("startSynthetricTransition() failed"); + } + } + + /** + * Returns whether this transition is backed by a real system transition or not. + */ + boolean isSyntheticTransition() { + return mTransition == SYNTHETIC_TRANSITION; + } + + /** + * Called when a synthetic transition is canceled. + */ + boolean cancelSyntheticTransition(String reason) { + if (!isSyntheticTransition()) { + return false; + } + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.cancelSyntheticTransition reason=%s", + mInstanceId, reason); + try { + // TODO(b/366021931): Notify the correct tasks once we build actual targets, and + // clean up leashes accordingly + mListener.onAnimationCanceled(new int[0], new TaskSnapshot[0]); + } catch (RemoteException e) { + Slog.e(TAG, "Error canceling previous recents animation", e); + } + cleanUp(); + return true; + } + + /** + * Called when a synthetic transition is finished. + * @return + */ + boolean finishSyntheticTransition() { + if (!isSyntheticTransition()) { + return false; + } + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.finishSyntheticTransition", mInstanceId); + // TODO(b/366021931): Clean up leashes accordingly + cleanUp(); + return true; + } + boolean start(TransitionInfo info, SurfaceControl.Transaction t, SurfaceControl.Transaction finishT, Transitions.TransitionFinishCallback finishCB) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, @@ -662,7 +841,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { // Set the callback once again so we can finish correctly. mFinishCB = finishCB; finishInner(true /* toHome */, false /* userLeave */, - null /* finishCb */); + null /* finishCb */, "takeOverAnimation"); }, updatedStates); }); } @@ -775,6 +954,20 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { // Don't consider order-only & non-leaf changes as changing apps. if (!TransitionUtil.isOrderOnly(change) && isLeafTask) { hasChangingApp = true; + // Check if the changing app is moving to top and fullscreen. This handles + // the case where we moved from desktop to recents and launching a desktop + // task in fullscreen. + if ((change.getFlags() & FLAG_MOVED_TO_TOP) != 0 + && taskInfo != null + && taskInfo.getWindowingMode() + == WINDOWING_MODE_FULLSCREEN) { + if (openingTasks == null) { + openingTasks = new ArrayList<>(); + openingTaskIsLeafs = new IntArray(); + } + openingTasks.add(change); + openingTaskIsLeafs.add(1); + } } else if (isLeafTask && taskInfo.topActivityType == ACTIVITY_TYPE_HOME && !isRecentsTask ) { // Unless it is a 3p launcher. This means that the 3p launcher was already @@ -796,7 +989,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { sendCancelWithSnapshots(); mExecutor.executeDelayed( () -> finishInner(true /* toHome */, false /* userLeaveHint */, - null /* finishCb */), 0); + null /* finishCb */, "merge"), 0); return; } if (recentsOpening != null) { @@ -991,7 +1184,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { return; } final int displayId = mInfo.getRootCount() > 0 ? mInfo.getRoot(0).getDisplayId() - : Display.DEFAULT_DISPLAY; + : DEFAULT_DISPLAY; // transient launches don't receive focus automatically. Since we are taking over // the gesture now, take focus explicitly. // This also moves recents back to top if the user gestured before a switch @@ -1008,10 +1201,6 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } @Override - public void setAnimationTargetsBehindSystemBars(boolean behindSystemBars) { - } - - @Override public void setFinishTaskTransaction(int taskId, PictureInPictureSurfaceTransaction finishTransaction, SurfaceControl overlay) { mExecutor.execute(() -> { @@ -1028,11 +1217,16 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { @Override @SuppressLint("NewApi") public void finish(boolean toHome, boolean sendUserLeaveHint, IResultReceiver finishCb) { - mExecutor.execute(() -> finishInner(toHome, sendUserLeaveHint, finishCb)); + mExecutor.execute(() -> finishInner(toHome, sendUserLeaveHint, finishCb, + "requested")); } private void finishInner(boolean toHome, boolean sendUserLeaveHint, - IResultReceiver runnerFinishCb) { + IResultReceiver runnerFinishCb, String reason) { + if (finishSyntheticTransition()) { + return; + } + if (mFinishCB == null) { Slog.e(TAG, "Duplicate call to finish"); return; @@ -1238,14 +1432,6 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } @Override - public void setDeferCancelUntilNextTransition(boolean defer, boolean screenshot) { - } - - @Override - public void cleanupScreenshot() { - } - - @Override public void setWillFinishToHome(boolean willFinishToHome) { mExecutor.execute(() -> { mWillFinishToHome = willFinishToHome; @@ -1253,14 +1439,6 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } /** - * @see IRecentsAnimationController#removeTask - */ - @Override - public boolean removeTask(int taskId) { - return false; - } - - /** * @see IRecentsAnimationController#detachNavigationBarFromApp */ @Override @@ -1276,13 +1454,6 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } }); } - - /** - * @see IRecentsAnimationController#animateNavigationBarToApp(long) - */ - @Override - public void animateNavigationBarToApp(long duration) { - } }; /** Utility class to track the state of a task as-seen by recents. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java index e8733ebd8f03..95874c8193c9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java @@ -24,7 +24,4 @@ public interface RecentsTransitionStateListener { /** Notifies whether the recents animation is running. */ default void onAnimationStateChanged(boolean running) { } - - /** Notifies that a recents shell transition has started. */ - default void onTransitionStarted(IBinder transition) {} } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt index 7c5f10a5bcca..e5bfccf0682e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt @@ -23,8 +23,8 @@ import android.util.ArrayMap import android.view.SurfaceControl import android.view.WindowManager import android.window.TransitionInfo -import com.android.window.flags.Flags.enableTaskStackObserverInShell import com.android.wm.shell.shared.TransitionUtil +import android.window.flags.DesktopModeFlags import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import dagger.Lazy @@ -62,7 +62,7 @@ class TaskStackTransitionObserver( startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction ) { - if (enableTaskStackObserverInShell()) { + if (DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue) { val taskInfoList = mutableListOf<RunningTaskInfo>() val transitionTypeList = mutableListOf<Int>() @@ -76,21 +76,40 @@ class TaskStackTransitionObserver( continue } + // Filter out changes that we care about if (change.mode == WindowManager.TRANSIT_OPEN) { change.taskInfo?.let { taskInfoList.add(it) } transitionTypeList.add(change.mode) } } - transitionToTransitionChanges.put( - transition, - TransitionChanges(taskInfoList, transitionTypeList) - ) + // Only add the transition to map if it has a change we care about + if (taskInfoList.isNotEmpty()) { + transitionToTransitionChanges.put( + transition, + TransitionChanges(taskInfoList, transitionTypeList) + ) + } } } override fun onTransitionStarting(transition: IBinder) {} - override fun onTransitionMerged(merged: IBinder, playing: IBinder) {} + override fun onTransitionMerged(merged: IBinder, playing: IBinder) { + val mergedTransitionChanges = + transitionToTransitionChanges.get(merged) + ?: + // We are adding changes of the merged transition to changes of the playing + // transition so if there is no changes nothing to do. + return + + transitionToTransitionChanges.remove(merged) + val playingTransitionChanges = transitionToTransitionChanges.get(playing) + if (playingTransitionChanges != null) { + playingTransitionChanges.merge(mergedTransitionChanges) + } else { + transitionToTransitionChanges.put(playing, mergedTransitionChanges) + } + } override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { val taskInfoList = @@ -138,6 +157,11 @@ class TaskStackTransitionObserver( private data class TransitionChanges( val taskInfoList: MutableList<RunningTaskInfo> = ArrayList(), - val transitionTypeList: MutableList<Int> = ArrayList() - ) + val transitionTypeList: MutableList<Int> = ArrayList(), + ) { + fun merge(transitionChanges: TransitionChanges) { + taskInfoList.addAll(transitionChanges.taskInfoList) + transitionTypeList.addAll(transitionChanges.transitionTypeList) + } + } } 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 0ca244c4b96a..59aa7926ce8f 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 @@ -109,35 +109,6 @@ interface ISplitScreen { in RemoteTransition remoteTransition, in InstanceId instanceId) = 17; /** - * Version of startTasks using legacy transition system. - */ - oneway void startTasksWithLegacyTransition(int taskId1, in Bundle options1, int taskId2, - in Bundle options2, int splitPosition, int snapPosition, - in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 11; - - /** - * Starts a pair of intent and task using legacy transition system. - */ - oneway void startIntentAndTaskWithLegacyTransition(in PendingIntent pendingIntent, int userId1, - in Bundle options1, int taskId, in Bundle options2, int splitPosition, int snapPosition, - in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 12; - - /** - * Starts a pair of shortcut and task using legacy transition system. - */ - oneway void startShortcutAndTaskWithLegacyTransition(in ShortcutInfo shortcutInfo, - in Bundle options1, int taskId, in Bundle options2, int splitPosition, int snapPosition, - in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 15; - - /** - * Start a pair of intents using legacy transition system. - */ - oneway void startIntentsWithLegacyTransition(in PendingIntent pendingIntent1, int userId1, - in ShortcutInfo shortcutInfo1, in Bundle options1, in PendingIntent pendingIntent2, - int userId2, in ShortcutInfo shortcutInfo2, in Bundle options2, int splitPosition, - int snapPosition, in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 18; - - /** * Start a pair of intents in one transition. */ oneway void startIntents(in PendingIntent pendingIntent1, int userId1, @@ -146,20 +117,6 @@ interface ISplitScreen { int snapPosition, in RemoteTransition remoteTransition, in InstanceId instanceId) = 19; /** - * Blocking call that notifies and gets additional split-screen targets when entering - * recents (for example: the dividerBar). - * @param appTargets apps that will be re-parented to display area - */ - RemoteAnimationTarget[] onGoingToRecentsLegacy(in RemoteAnimationTarget[] appTargets) = 13; - - /** - * Blocking call that notifies and gets additional split-screen targets when entering - * recents (for example: the dividerBar). Different than the method above in that this one - * does not expect split to currently be running. - */ - RemoteAnimationTarget[] onStartingSplitLegacy(in RemoteAnimationTarget[] appTargets) = 14; - - /** * Reverse the split. */ oneway void switchSplitPosition() = 22; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java deleted file mode 100644 index 64e26dbd70be..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.splitscreen; - -import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN; - -import android.content.Context; -import android.view.SurfaceSession; -import android.window.WindowContainerToken; -import android.window.WindowContainerTransaction; - -import com.android.internal.protolog.common.ProtoLog; -import com.android.launcher3.icons.IconProvider; -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.windowdecor.WindowDecorViewModel; - -import java.util.Optional; - -/** - * Main stage for split-screen mode. When split-screen is active all standard activity types launch - * on the main stage, except for task that are explicitly pinned to the {@link SideStage}. - * @see StageCoordinator - */ -class MainStage extends StageTaskListener { - private boolean mIsActive = false; - - MainStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId, - StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, - SurfaceSession surfaceSession, IconProvider iconProvider, - Optional<WindowDecorViewModel> windowDecorViewModel) { - super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, - iconProvider, windowDecorViewModel); - } - - boolean isActive() { - return mIsActive; - } - - void activate(WindowContainerTransaction wct, boolean includingTopTask) { - if (mIsActive) return; - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "activate: main stage includingTopTask=%b", - includingTopTask); - - if (includingTopTask) { - reparentTopTask(wct); - } - - mIsActive = true; - } - - void deactivate(WindowContainerTransaction wct) { - deactivate(wct, false /* toTop */); - } - - void deactivate(WindowContainerTransaction wct, boolean toTop) { - if (!mIsActive) return; - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "deactivate: main stage toTop=%b rootTaskInfo=%s", - toTop, mRootTaskInfo); - mIsActive = false; - - if (mRootTaskInfo == null) return; - final WindowContainerToken rootToken = mRootTaskInfo.token; - wct.reparentTasks( - rootToken, - null /* newParent */, - null /* windowingModes */, - null /* activityTypes */, - toTop); - } -} 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 deleted file mode 100644 index f5fbae55960a..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.splitscreen; - -import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN; - -import android.app.ActivityManager; -import android.content.Context; -import android.view.SurfaceSession; -import android.window.WindowContainerToken; -import android.window.WindowContainerTransaction; - -import com.android.internal.protolog.common.ProtoLog; -import com.android.launcher3.icons.IconProvider; -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.windowdecor.WindowDecorViewModel; - -import java.util.Optional; - -/** - * 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(); - - SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId, - StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, - SurfaceSession surfaceSession, IconProvider iconProvider, - Optional<WindowDecorViewModel> windowDecorViewModel) { - super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, - iconProvider, windowDecorViewModel); - } - - boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) { - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "remove all side stage tasks: childCount=%d toTop=%b", - mChildrenTaskInfo.size(), toTop); - if (mChildrenTaskInfo.size() == 0) return false; - wct.reparentTasks( - mRootTaskInfo.token, - null /* newParent */, - null /* windowingModes */, - null /* activityTypes */, - toTop); - return true; - } - - boolean removeTask(int taskId, WindowContainerToken newParent, WindowContainerTransaction wct) { - final ActivityManager.RunningTaskInfo task = mChildrenTaskInfo.get(taskId); - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "remove side stage task: task=%d exists=%b", taskId, - task != null); - if (task == null) return false; - wct.reparent(task.token, newParent, false /* onTop */); - return true; - } -} 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 8df287d12cbc..b36b1f84d21f 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 @@ -25,9 +25,9 @@ import android.os.Bundle; import android.window.RemoteTransition; import com.android.internal.logging.InstanceId; -import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; -import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.shared.annotations.ExternalThread; +import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; +import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; import java.util.concurrent.Executor; @@ -44,13 +44,13 @@ public interface SplitScreen { int STAGE_TYPE_UNDEFINED = -1; /** * The main stage type. - * @see MainStage + * @see StageTaskListener */ int STAGE_TYPE_MAIN = 0; /** * The side stage type. - * @see SideStage + * @see StageTaskListener */ int STAGE_TYPE_SIDE = 1; @@ -113,6 +113,9 @@ public interface SplitScreen { /** Called when device waking up finished. */ void onFinishedWakingUp(); + /** Called when device starts going to sleep (screen off). */ + void onStartedGoingToSleep(); + /** Called when requested to go to fullscreen from the current active split app. */ void goToFullscreenFromSplit(); 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 dd219d32bbaa..87b661d340ed 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 @@ -19,24 +19,24 @@ package com.android.wm.shell.splitscreen; import static android.app.ActivityManager.START_SUCCESS; import static android.app.ActivityManager.START_TASK_TO_FRONT; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; import static android.view.Display.DEFAULT_DISPLAY; -import static android.view.RemoteAnimationTarget.MODE_OPENING; import static com.android.wm.shell.common.MultiInstanceHelper.getComponent; import static com.android.wm.shell.common.MultiInstanceHelper.getShortcutComponent; import static com.android.wm.shell.common.MultiInstanceHelper.samePackage; -import static com.android.wm.shell.common.split.SplitScreenConstants.KEY_EXTRA_WIDGET_INTENT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.common.split.SplitScreenUtils.isValidToSplit; import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition; import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN; +import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN; +import static com.android.wm.shell.shared.split.SplitScreenConstants.KEY_EXTRA_WIDGET_INTENT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; -import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN; -import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; import android.app.ActivityManager; import android.app.ActivityOptions; @@ -50,6 +50,7 @@ import android.content.pm.LauncherApps; import android.content.pm.ShortcutInfo; import android.graphics.Rect; import android.os.Bundle; +import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; import android.util.ArrayMap; @@ -60,10 +61,10 @@ import android.view.IRemoteAnimationRunner; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.view.WindowManager; import android.widget.Toast; import android.window.RemoteTransition; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; @@ -73,7 +74,7 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; @@ -88,16 +89,16 @@ import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; -import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.common.split.SplitScreenUtils; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.draganddrop.DragAndDropController; -import com.android.wm.shell.draganddrop.DragAndDropPolicy; +import com.android.wm.shell.draganddrop.SplitDragPolicy; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.annotations.ExternalThread; +import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; +import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.splitscreen.SplitScreen.StageType; import com.android.wm.shell.sysui.KeyguardChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -120,7 +121,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * @see StageCoordinator */ // TODO(b/198577848): Implement split screen flicker test to consolidate CUJ of split screen. -public class SplitScreenController implements DragAndDropPolicy.Starter, +public class SplitScreenController implements SplitDragPolicy.Starter, RemoteCallable<SplitScreenController>, KeyguardChangeListener { private static final String TAG = SplitScreenController.class.getSimpleName(); @@ -179,6 +180,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, private final LauncherApps mLauncherApps; private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer; private final ShellExecutor mMainExecutor; + private final Handler mMainHandler; private final SplitScreenImpl mImpl = new SplitScreenImpl(); private final DisplayController mDisplayController; private final DisplayImeController mDisplayImeController; @@ -197,11 +199,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @VisibleForTesting StageCoordinator mStageCoordinator; - // Only used for the legacy recents animation from splitscreen to allow the tasks to be animated - // outside the bounds of the roots by being reparented into a higher level fullscreen container - private SurfaceControl mGoingToRecentsTasksLayer; - private SurfaceControl mStartingSplitTasksLayer; - /** * @param stageCoordinator if null, a stage coordinator will be created when this controller is * initialized. Can be non-null for testing purposes. @@ -226,7 +223,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, Optional<DesktopTasksController> desktopTasksController, @Nullable StageCoordinator stageCoordinator, MultiInstanceHelper multiInstanceHelper, - ShellExecutor mainExecutor) { + ShellExecutor mainExecutor, + Handler mainHandler) { mShellCommandHandler = shellCommandHandler; mShellController = shellController; mTaskOrganizer = shellTaskOrganizer; @@ -235,6 +233,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mLauncherApps = context.getSystemService(LauncherApps.class); mRootTDAOrganizer = rootTDAOrganizer; mMainExecutor = mainExecutor; + mMainHandler = mainHandler; mDisplayController = displayController; mDisplayImeController = displayImeController; mDisplayInsetsController = displayInsetsController; @@ -291,7 +290,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mDisplayController, mDisplayImeController, mDisplayInsetsController, mTransitions, mTransactionPool, mIconProvider, - mMainExecutor, mRecentTasksOptional, mLaunchAdjacentController, + mMainExecutor, mMainHandler, mRecentTasksOptional, mLaunchAdjacentController, mWindowDecorViewModel); } @@ -426,6 +425,20 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mStageCoordinator.clearSplitPairedInRecents(reason); } + /** + * Determines which split position a new instance of a task should take. + * @param callingTask The task requesting a new instance. + * @return the split position of the new instance + */ + public int determineNewInstancePosition(@NonNull ActivityManager.RunningTaskInfo callingTask) { + if (callingTask.getWindowingMode() == WINDOWING_MODE_FULLSCREEN + || getSplitPosition(callingTask.taskId) == SPLIT_POSITION_TOP_OR_LEFT) { + return SPLIT_POSITION_BOTTOM_OR_RIGHT; + } else { + return SPLIT_POSITION_TOP_OR_LEFT; + } + } + public void enterSplitScreen(int taskId, boolean leftOrTop) { enterSplitScreen(taskId, leftOrTop, new WindowContainerTransaction()); } @@ -437,23 +450,23 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } public void exitSplitScreen(int toTopTaskId, @ExitReason int exitReason) { - if (ENABLE_SHELL_TRANSITIONS) { - mStageCoordinator.dismissSplitScreen(toTopTaskId, exitReason); - } else { - mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason); - } + mStageCoordinator.dismissSplitScreen(toTopTaskId, exitReason); } @Override public void onKeyguardVisibilityChanged(boolean visible, boolean occluded, boolean animatingDismiss) { - mStageCoordinator.onKeyguardVisibilityChanged(visible); + mStageCoordinator.onKeyguardStateChanged(visible, occluded); } public void onFinishedWakingUp() { mStageCoordinator.onFinishedWakingUp(); } + public void onStartedGoingToSleep() { + mStageCoordinator.onStartedGoingToSleep(); + } + public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { mStageCoordinator.exitSplitScreenOnHide(exitSplitScreenOnHide); } @@ -526,7 +539,15 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mStageCoordinator.requestEnterSplitSelect(taskInfo, wct, splitPosition, taskBounds); } - public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) { + /** + * Starts an existing task into split. + * TODO(b/351900580): We should remove this path and use StageCoordinator#startTask() instead + * @param hideTaskToken is not supported. + */ + public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options, + @Nullable WindowContainerToken hideTaskToken) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Legacy startTask does not support hide task token"); final int[] result = new int[1]; IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() { @Override @@ -574,7 +595,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, */ public void startShortcut(String packageName, String shortcutId, @SplitPosition int position, @Nullable Bundle options, UserHandle user, @NonNull InstanceId instanceId) { - mStageCoordinator.onRequestToSplit(instanceId, ENTER_REASON_LAUNCHER); + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startShortcut: reason=%d", ENTER_REASON_LAUNCHER); + mStageCoordinator.getLogger().enterRequested(instanceId, ENTER_REASON_LAUNCHER); startShortcut(packageName, shortcutId, position, options, user); } @@ -584,8 +606,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, if (options == null) options = new Bundle(); final ActivityOptions activityOptions = ActivityOptions.fromBundle(options); - if (samePackage(packageName, getPackageName(reverseSplitPosition(position)), - user.getIdentifier(), getUserId(reverseSplitPosition(position)))) { + if (samePackage(packageName, getPackageName(reverseSplitPosition(position), null), + user.getIdentifier(), getUserId(reverseSplitPosition(position), null))) { if (mMultiInstanceHelpher.supportsMultiInstanceSplit( getShortcutComponent(packageName, shortcutId, user, mLauncherApps))) { activityOptions.setApplyMultipleTaskFlagForShortcut(true); @@ -608,37 +630,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, activityOptions.toBundle(), user); } - void startShortcutAndTaskWithLegacyTransition(@NonNull ShortcutInfo shortcutInfo, - @Nullable Bundle options1, int taskId, @Nullable Bundle options2, - @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, - RemoteAnimationAdapter adapter, InstanceId instanceId) { - if (options1 == null) options1 = new Bundle(); - final ActivityOptions activityOptions = ActivityOptions.fromBundle(options1); - - final String packageName1 = shortcutInfo.getPackage(); - final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer); - final int userId1 = shortcutInfo.getUserId(); - final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer); - if (samePackage(packageName1, packageName2, userId1, userId2)) { - if (mMultiInstanceHelpher.supportsMultiInstanceSplit(shortcutInfo.getActivity())) { - activityOptions.setApplyMultipleTaskFlagForShortcut(true); - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); - } else { - taskId = INVALID_TASK_ID; - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, - "Cancel entering split as not supporting multi-instances"); - Log.w(TAG, splitFailureMessage("startShortcutAndTaskWithLegacyTransition", - "app package " + packageName1 + " does not support multi-instance")); - Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text, - Toast.LENGTH_SHORT).show(); - } - } - - mStageCoordinator.startShortcutAndTaskWithLegacyTransition(shortcutInfo, - activityOptions.toBundle(), taskId, options2, splitPosition, snapPosition, adapter, - instanceId); - } - void startShortcutAndTask(@NonNull ShortcutInfo shortcutInfo, @Nullable Bundle options1, int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition, @@ -676,37 +667,13 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, * See {@link #startIntent(PendingIntent, int, Intent, int, Bundle)} * @param instanceId to be used by {@link SplitscreenEventLogger} */ - public void startIntent(PendingIntent intent, int userId, @Nullable Intent fillInIntent, - @SplitPosition int position, @Nullable Bundle options, @NonNull InstanceId instanceId) { - mStageCoordinator.onRequestToSplit(instanceId, ENTER_REASON_LAUNCHER); - startIntent(intent, userId, fillInIntent, position, options); - } - - private void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, int userId1, - @Nullable Bundle options1, int taskId, @Nullable Bundle options2, - @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, - RemoteAnimationAdapter adapter, InstanceId instanceId) { - Intent fillInIntent = null; - final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent); - final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer); - final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer); - if (samePackage(packageName1, packageName2, userId1, userId2)) { - if (mMultiInstanceHelpher.supportsMultiInstanceSplit(getComponent(pendingIntent))) { - fillInIntent = new Intent(); - fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); - } else { - taskId = INVALID_TASK_ID; - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, - "Cancel entering split as not supporting multi-instances"); - Log.w(TAG, splitFailureMessage("startIntentAndTaskWithLegacyTransition", - "app package " + packageName1 + " does not support multi-instance")); - Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text, - Toast.LENGTH_SHORT).show(); - } - } - mStageCoordinator.startIntentAndTaskWithLegacyTransition(pendingIntent, fillInIntent, - options1, taskId, options2, splitPosition, snapPosition, adapter, instanceId); + public void startIntentWithInstanceId(PendingIntent intent, int userId, + @Nullable Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options, + @NonNull InstanceId instanceId) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startIntentWithInstanceId: reason=%d", + ENTER_REASON_LAUNCHER); + mStageCoordinator.getLogger().enterRequested(instanceId, ENTER_REASON_LAUNCHER); + startIntent(intent, userId, fillInIntent, position, options, null /* hideTaskToken */); } private void startIntentAndTask(PendingIntent pendingIntent, int userId1, @@ -745,38 +712,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, options2, splitPosition, snapPosition, remoteTransition, instanceId); } - private void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, int userId1, - @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1, - PendingIntent pendingIntent2, int userId2, @Nullable ShortcutInfo shortcutInfo2, - @Nullable Bundle options2, @SplitPosition int splitPosition, - @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter, - InstanceId instanceId) { - Intent fillInIntent1 = null; - Intent fillInIntent2 = null; - final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent1); - final String packageName2 = SplitScreenUtils.getPackageName(pendingIntent2); - if (samePackage(packageName1, packageName2, userId1, userId2)) { - if (mMultiInstanceHelpher.supportsMultiInstanceSplit(getComponent(pendingIntent1))) { - fillInIntent1 = new Intent(); - fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); - fillInIntent2 = new Intent(); - fillInIntent2.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); - } else { - pendingIntent2 = null; - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, - "Cancel entering split as not supporting multi-instances"); - Log.w(TAG, splitFailureMessage("startIntentsWithLegacyTransition", - "app package " + packageName1 + " does not support multi-instance")); - Toast.makeText(mContext, R.string.dock_multi_instances_not_supported_text, - Toast.LENGTH_SHORT).show(); - } - } - mStageCoordinator.startIntentsWithLegacyTransition(pendingIntent1, fillInIntent1, - shortcutInfo1, options1, pendingIntent2, fillInIntent2, shortcutInfo2, options2, - splitPosition, snapPosition, adapter, instanceId); - } - private void startIntents(PendingIntent pendingIntent1, int userId1, @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1, PendingIntent pendingIntent2, int userId2, @Nullable ShortcutInfo shortcutInfo2, @@ -825,9 +760,15 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, instanceId); } + /** + * Starts the given intent into split. + * @param hideTaskToken If non-null, a task matching this token will be moved to back in the + * same window container transaction as the starting of the intent. + */ @Override public void startIntent(PendingIntent intent, int userId1, @Nullable Intent fillInIntent, - @SplitPosition int position, @Nullable Bundle options) { + @SplitPosition int position, @Nullable Bundle options, + @Nullable WindowContainerToken hideTaskToken) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "startIntent(): intent=%s user=%d fillInIntent=%s position=%d", intent, userId1, fillInIntent, position); @@ -838,24 +779,22 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION); final String packageName1 = SplitScreenUtils.getPackageName(intent); - final String packageName2 = getPackageName(reverseSplitPosition(position)); - final int userId2 = getUserId(reverseSplitPosition(position)); + final String packageName2 = getPackageName(reverseSplitPosition(position), hideTaskToken); + final int userId2 = getUserId(reverseSplitPosition(position), hideTaskToken); final ComponentName component = intent.getIntent().getComponent(); // To prevent accumulating large number of instances in the background, reuse task // in the background. If we don't explicitly reuse, new may be created even if the app // isn't multi-instance because WM won't automatically remove/reuse the previous instance final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional - .map(recentTasks -> recentTasks.findTaskInBackground(component, userId1)) + .map(recentTasks -> recentTasks.findTaskInBackground(component, userId1, + hideTaskToken)) .orElse(null); if (taskInfo != null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Found suitable background task=%s", taskInfo); - if (ENABLE_SHELL_TRANSITIONS) { - mStageCoordinator.startTask(taskInfo.taskId, position, options); - } else { - startTask(taskInfo.taskId, position, options); - } + mStageCoordinator.startTask(taskInfo.taskId, position, options, hideTaskToken); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Start task in background"); return; } @@ -879,19 +818,23 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } } - mStageCoordinator.startIntent(intent, fillInIntent, position, options); + mStageCoordinator.startIntent(intent, fillInIntent, position, options, hideTaskToken); } - /** Retrieve package name of a specific split position if split screen is activated, otherwise - * returns the package name of the top running task. */ + /** + * Retrieve package name of a specific split position if split screen is activated, otherwise + * returns the package name of the top running task. + * TODO(b/351900580): Merge this with getUserId() so we don't make multiple binder calls + */ @Nullable - private String getPackageName(@SplitPosition int position) { + private String getPackageName(@SplitPosition int position, + @Nullable WindowContainerToken ignoreTaskToken) { ActivityManager.RunningTaskInfo taskInfo; if (isSplitScreenVisible()) { taskInfo = getTaskInfo(position); } else { taskInfo = mRecentTasksOptional - .map(recentTasks -> recentTasks.getTopRunningTask()) + .map(recentTasks -> recentTasks.getTopRunningTask(ignoreTaskToken)) .orElse(null); if (!isValidToSplit(taskInfo)) { return null; @@ -901,15 +844,19 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return taskInfo != null ? SplitScreenUtils.getPackageName(taskInfo.baseIntent) : null; } - /** Retrieve user id of a specific split position if split screen is activated, otherwise - * returns the user id of the top running task. */ - private int getUserId(@SplitPosition int position) { + /** + * Retrieve user id of a specific split position if split screen is activated, otherwise + * returns the user id of the top running task. + * TODO: Merge this with getPackageName() so we don't make multiple binder calls + */ + private int getUserId(@SplitPosition int position, + @Nullable WindowContainerToken ignoreTaskToken) { ActivityManager.RunningTaskInfo taskInfo; if (isSplitScreenVisible()) { taskInfo = getTaskInfo(position); } else { taskInfo = mRecentTasksOptional - .map(recentTasks -> recentTasks.getTopRunningTask()) + .map(recentTasks -> recentTasks.getTopRunningTask(ignoreTaskToken)) .orElse(null); if (!isValidToSplit(taskInfo)) { return -1; @@ -947,66 +894,9 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return fillInIntent2; } - RemoteAnimationTarget[] onGoingToRecentsLegacy(RemoteAnimationTarget[] apps) { - if (ENABLE_SHELL_TRANSITIONS) return null; - - if (isSplitScreenVisible()) { - // Evict child tasks except the top visible one under split root to ensure it could be - // launched as full screen when switching to it on recents. - final WindowContainerTransaction wct = new WindowContainerTransaction(); - mStageCoordinator.prepareEvictInvisibleChildTasks(wct); - mSyncQueue.queue(wct); - } else { - return null; - } - - SurfaceControl.Transaction t = mTransactionPool.acquire(); - if (mGoingToRecentsTasksLayer != null) { - t.remove(mGoingToRecentsTasksLayer); - } - mGoingToRecentsTasksLayer = reparentSplitTasksForAnimation(apps, t, - "SplitScreenController#onGoingToRecentsLegacy" /* callsite */); - t.apply(); - mTransactionPool.release(t); - - return new RemoteAnimationTarget[]{mStageCoordinator.getDividerBarLegacyTarget()}; - } - - RemoteAnimationTarget[] onStartingSplitLegacy(RemoteAnimationTarget[] apps) { - if (ENABLE_SHELL_TRANSITIONS) return null; - - int openingApps = 0; - for (int i = 0; i < apps.length; ++i) { - if (apps[i].mode == MODE_OPENING) openingApps++; - } - if (openingApps < 2) { - // Not having enough apps to enter split screen - return null; - } - - SurfaceControl.Transaction t = mTransactionPool.acquire(); - if (mStartingSplitTasksLayer != null) { - t.remove(mStartingSplitTasksLayer); - } - mStartingSplitTasksLayer = reparentSplitTasksForAnimation(apps, t, - "SplitScreenController#onStartingSplitLegacy" /* callsite */); - t.apply(); - mTransactionPool.release(t); - - try { - return new RemoteAnimationTarget[]{mStageCoordinator.getDividerBarLegacyTarget()}; - } finally { - for (RemoteAnimationTarget appTarget : apps) { - if (appTarget.leash != null) { - appTarget.leash.release(); - } - } - } - } - private SurfaceControl reparentSplitTasksForAnimation(RemoteAnimationTarget[] apps, SurfaceControl.Transaction t, String callsite) { - final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + final SurfaceControl.Builder builder = new SurfaceControl.Builder() .setContainerLayer() .setName("RecentsAnimationSplitTasks") .setHidden(false) @@ -1176,6 +1066,11 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override + public void onStartedGoingToSleep() { + mMainExecutor.execute(SplitScreenController.this::onStartedGoingToSleep); + } + + @Override public void goToFullscreenFromSplit() { mMainExecutor.execute(SplitScreenController.this::goToFullscreenFromSplit); } @@ -1290,42 +1185,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @Override public void startTask(int taskId, int position, @Nullable Bundle options) { executeRemoteCallWithTaskPermission(mController, "startTask", - (controller) -> controller.startTask(taskId, position, options)); - } - - @Override - public void startTasksWithLegacyTransition(int taskId1, @Nullable Bundle options1, - int taskId2, @Nullable Bundle options2, @SplitPosition int splitPosition, - @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter, - InstanceId instanceId) { - executeRemoteCallWithTaskPermission(mController, "startTasks", - (controller) -> controller.mStageCoordinator.startTasksWithLegacyTransition( - taskId1, options1, taskId2, options2, splitPosition, snapPosition, - adapter, instanceId)); - } - - @Override - public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, int userId1, - Bundle options1, int taskId, Bundle options2, int splitPosition, - @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter, - InstanceId instanceId) { - executeRemoteCallWithTaskPermission(mController, - "startIntentAndTaskWithLegacyTransition", (controller) -> - controller.startIntentAndTaskWithLegacyTransition(pendingIntent, - userId1, options1, taskId, options2, splitPosition, - snapPosition, adapter, instanceId)); - } - - @Override - public void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo, - @Nullable Bundle options1, int taskId, @Nullable Bundle options2, - @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, - RemoteAnimationAdapter adapter, InstanceId instanceId) { - executeRemoteCallWithTaskPermission(mController, - "startShortcutAndTaskWithLegacyTransition", (controller) -> - controller.startShortcutAndTaskWithLegacyTransition( - shortcutInfo, options1, taskId, options2, splitPosition, - snapPosition, adapter, instanceId)); + (controller) -> controller.startTask(taskId, position, options, + null /* hideTaskToken */)); } @Override @@ -1361,21 +1222,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override - public void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, int userId1, - @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1, - PendingIntent pendingIntent2, int userId2, @Nullable ShortcutInfo shortcutInfo2, - @Nullable Bundle options2, @SplitPosition int splitPosition, - @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter, - InstanceId instanceId) { - executeRemoteCallWithTaskPermission(mController, "startIntentsWithLegacyTransition", - (controller) -> - controller.startIntentsWithLegacyTransition(pendingIntent1, userId1, - shortcutInfo1, options1, pendingIntent2, userId2, shortcutInfo2, - options2, splitPosition, snapPosition, adapter, instanceId) - ); - } - - @Override public void startIntents(PendingIntent pendingIntent1, int userId1, @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1, PendingIntent pendingIntent2, int userId2, @Nullable ShortcutInfo shortcutInfo2, @@ -1402,26 +1248,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, public void startIntent(PendingIntent intent, int userId, Intent fillInIntent, int position, @Nullable Bundle options, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startIntent", - (controller) -> controller.startIntent(intent, userId, fillInIntent, position, - options, instanceId)); - } - - @Override - public RemoteAnimationTarget[] onGoingToRecentsLegacy(RemoteAnimationTarget[] apps) { - final RemoteAnimationTarget[][] out = new RemoteAnimationTarget[][]{null}; - executeRemoteCallWithTaskPermission(mController, "onGoingToRecentsLegacy", - (controller) -> out[0] = controller.onGoingToRecentsLegacy(apps), - true /* blocking */); - return out[0]; - } - - @Override - public RemoteAnimationTarget[] onStartingSplitLegacy(RemoteAnimationTarget[] apps) { - final RemoteAnimationTarget[][] out = new RemoteAnimationTarget[][]{null}; - executeRemoteCallWithTaskPermission(mController, "onStartingSplitLegacy", - (controller) -> out[0] = controller.onStartingSplitLegacy(apps), - true /* blocking */); - return out[0]; + (controller) -> controller.startIntentWithInstanceId(intent, userId, + fillInIntent, position, options, instanceId)); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java index af11ebc515d7..e1b474d9804a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java @@ -16,7 +16,7 @@ package com.android.wm.shell.splitscreen; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_UNKNOWN; import com.android.wm.shell.sysui.ShellCommandHandler; 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 b31ef2b6ad4e..840049412db4 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 @@ -21,12 +21,12 @@ import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; -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.common.split.SplitScreenConstants.FADE_DURATION; -import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TRANSITIONS; +import static com.android.wm.shell.shared.animation.Interpolators.ALPHA_IN; +import static com.android.wm.shell.shared.animation.Interpolators.ALPHA_OUT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.FADE_DURATION; +import static com.android.wm.shell.shared.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; import static com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER; import static com.android.wm.shell.splitscreen.SplitScreenController.exitReasonToString; @@ -46,10 +46,10 @@ import android.window.TransitionInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; -import com.android.internal.protolog.common.ProtoLog; -import com.android.wm.shell.common.TransactionPool; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.split.SplitDecorManager; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.transition.OneShotRemoteHandler; import com.android.wm.shell.transition.Transitions; @@ -439,9 +439,9 @@ class SplitScreenTransitions { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "setResizeTransition: hasPendingResize=%b", mPendingResize != null); if (mPendingResize != null) { + mPendingResize.cancel(null); mainDecor.cancelRunningAnimations(); sideDecor.cancelRunningAnimations(); - mPendingResize.cancel(null); mAnimations.clear(); onFinish(null /* wct */); } @@ -504,7 +504,9 @@ class SplitScreenTransitions { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTransitionConsumed for passThrough transition"); } - // TODO: handle transition consumed for active remote handler + if (mActiveRemoteHandler != null) { + mActiveRemoteHandler.onTransitionConsumed(transition, aborted, finishT); + } } void onFinish(WindowContainerTransaction wct) { 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 index a0bf843444df..2033902f03c7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java @@ -24,6 +24,7 @@ import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED_ import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__CHILD_TASK_ENTER_PIP; 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__FULLSCREEN_REQUEST; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_SHORTCUT; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DESKTOP_MODE; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RECREATE_SPLIT; @@ -32,8 +33,9 @@ import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED_ import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP; import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_DRAG; import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_LAUNCHER; import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_MULTI_INSTANCE; @@ -44,6 +46,7 @@ import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DESKTOP_MODE; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_REQUEST; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RECREATE_SPLIT; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME; @@ -57,8 +60,9 @@ import android.util.Slog; import com.android.internal.logging.InstanceId; import com.android.internal.logging.InstanceIdSequence; +import com.android.internal.protolog.ProtoLog; import com.android.internal.util.FrameworkStatsLog; -import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; +import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.splitscreen.SplitScreenController.ExitReason; /** @@ -133,6 +137,11 @@ public class SplitscreenEventLogger { @SplitPosition int mainStagePosition, int mainStageUid, @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) { + if (hasStartedSession()) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "logEnter: no-op, previous session has not ended"); + return; + } + mLoggerSessionId = mIdSequence.newInstanceId(); int enterReason = getLoggerEnterReason(isLandscape); updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), @@ -140,6 +149,14 @@ public class SplitscreenEventLogger { updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), sideStageUid); updateSplitRatioState(splitRatio); + + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "logEnter: enterReason=%d splitRatio=%f " + + "mainStagePosition=%d mainStageUid=%d sideStagePosition=%d " + + "sideStageUid=%d isLandscape=%b mEnterSessionId=%d mLoggerSessionId=%d", + enterReason, splitRatio, mLastMainStagePosition, mLastMainStageUid, + mLastSideStagePosition, mLastSideStageUid, isLandscape, + mEnterSessionId != null ? mEnterSessionId.getId() : 0, mLoggerSessionId.getId()); + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__ENTER, enterReason, @@ -196,6 +213,8 @@ public class SplitscreenEventLogger { return SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_SHORTCUT; case EXIT_REASON_DESKTOP_MODE: return SPLITSCREEN_UICHANGED__EXIT_REASON__DESKTOP_MODE; + case EXIT_REASON_FULLSCREEN_REQUEST: + return SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_REQUEST; case EXIT_REASON_UNKNOWN: // Fall through default: @@ -212,14 +231,25 @@ public class SplitscreenEventLogger { @SplitPosition int mainStagePosition, int mainStageUid, @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) { if (mLoggerSessionId == null) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "logExit: no-op, mLoggerSessionId is null"); // Ignore changes until we've started logging the session return; } if ((mainStagePosition != SPLIT_POSITION_UNDEFINED && sideStagePosition != SPLIT_POSITION_UNDEFINED) || (mainStageUid != 0 && sideStageUid != 0)) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, + "logExit: no-op, only main or side stage should be set, not both/none"); throw new IllegalArgumentException("Only main or side stage should be set"); } + + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "logExit: exitReason=%d mainStagePosition=%d" + + " mainStageUid=%d sideStagePosition=%d sideStageUid=%d isLandscape=%b" + + " mLoggerSessionId=%d", getLoggerExitReason(exitReason), + getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), mainStageUid, + getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), sideStageUid, + isLandscape, mLoggerSessionId.getId()); + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__EXIT, 0 /* enterReason */, @@ -304,25 +334,34 @@ public class SplitscreenEventLogger { */ public void logResize(float splitRatio) { if (mLoggerSessionId == null) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "logResize: no-op, mLoggerSessionId is null"); // Ignore changes until we've started logging the session return; } if (splitRatio <= 0f || splitRatio >= 1f) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, + "logResize: no-op, splitRatio indicates that user is dismissing, not resizing"); // Don't bother reporting resizes that end up dismissing the split, that will be logged // via the exit event return; } if (!updateSplitRatioState(splitRatio)) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "logResize: no-op, split ratio was not changed"); // Ignore if there are no user perceived changes return; } + + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "logResize: splitRatio=%f mLoggerSessionId=%d", + mLastSplitRatio, mLoggerSessionId.getId()); FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__RESIZE, 0 /* enterReason */, 0 /* exitReason */, mLastSplitRatio, - 0 /* mainStagePosition */, 0 /* mainStageUid */, - 0 /* sideStagePosition */, 0 /* sideStageUid */, + mLastMainStagePosition, + mLastMainStageUid, + mLastSideStagePosition, + mLastSideStageUid, 0 /* dragInstanceId */, mLoggerSessionId.getId()); } @@ -333,6 +372,7 @@ public class SplitscreenEventLogger { public void logSwap(@SplitPosition int mainStagePosition, int mainStageUid, @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) { if (mLoggerSessionId == null) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "logSwap: no-op, mLoggerSessionId is null"); // Ignore changes until we've started logging the session return; } @@ -341,6 +381,11 @@ public class SplitscreenEventLogger { mainStageUid); updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), sideStageUid); + + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "logSwap: mainStagePosition=%d mainStageUid=%d " + + "sideStagePosition=%d sideStageUid=%d mLoggerSessionId=%d", + mLastMainStagePosition, mLastMainStageUid, mLastSideStagePosition, + mLastSideStageUid, mLoggerSessionId.getId()); FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__SWAP, 0 /* enterReason */, 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 4f22c14c139d..3f1eb068c4b9 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 @@ -16,16 +16,15 @@ package com.android.wm.shell.splitscreen; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT; import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; -import static android.view.RemoteAnimationTarget.MODE_OPENING; -import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE; @@ -36,17 +35,16 @@ import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER; -import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; -import static com.android.wm.shell.common.split.SplitScreenConstants.splitPositionToString; -import static com.android.wm.shell.common.split.SplitScreenUtils.getResizingBackgroundColor; import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition; import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN; import static com.android.wm.shell.shared.TransitionUtil.isClosingType; import static com.android.wm.shell.shared.TransitionUtil.isOpeningType; +import static com.android.wm.shell.shared.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.shared.split.SplitScreenConstants.splitPositionToString; 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; @@ -60,14 +58,12 @@ import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_REQUEST; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT; -import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RECREATE_SPLIT; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_ROOT_TASK_VANISHED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_UNKNOWN; import static com.android.wm.shell.splitscreen.SplitScreenController.exitReasonToString; import static com.android.wm.shell.transition.MixedTransitionHelper.getPipReplacingChange; -import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; 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; @@ -82,7 +78,6 @@ import android.app.ActivityOptions; import android.app.IActivityTaskManager; import android.app.PendingIntent; import android.app.TaskInfo; -import android.app.WindowConfiguration; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; @@ -92,6 +87,7 @@ import android.graphics.Rect; import android.hardware.devicestate.DeviceStateManager; import android.os.Bundle; import android.os.Debug; +import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; @@ -107,7 +103,6 @@ import android.view.IRemoteAnimationRunner; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.view.WindowManager; import android.widget.Toast; import android.window.DisplayAreaInfo; @@ -119,8 +114,8 @@ import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; -import com.android.internal.protolog.common.ProtoLog; -import com.android.internal.util.ArrayUtils; +import com.android.internal.policy.FoldLockSettingsObserver; +import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; @@ -128,25 +123,23 @@ 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.LaunchAdjacentController; -import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.split.SplitDecorManager; import com.android.wm.shell.common.split.SplitLayout; -import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; -import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.common.split.SplitScreenUtils; import com.android.wm.shell.common.split.SplitWindowManager; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.TransitionUtil; +import com.android.wm.shell.shared.split.SplitBounds; +import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; +import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.splitscreen.SplitScreen.StageType; import com.android.wm.shell.splitscreen.SplitScreenController.ExitReason; -import com.android.wm.shell.splitscreen.SplitScreenController.SplitEnterReason; import com.android.wm.shell.transition.DefaultMixedHandler; -import com.android.wm.shell.transition.LegacyTransitions; import com.android.wm.shell.transition.Transitions; -import com.android.wm.shell.util.SplitBounds; import com.android.wm.shell.windowdecor.WindowDecorViewModel; import dalvik.annotation.optimization.NeverCompile; @@ -160,14 +153,12 @@ import java.util.Set; import java.util.concurrent.Executor; /** - * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and - * {@link SideStage} stages. + * Coordinates the staging (visibility, sizing, ...) of the split-screen stages. * Some high-level rules: - * - The {@link StageCoordinator} is only considered active if the {@link SideStage} contains at + * - The {@link StageCoordinator} is only considered active if the other stages contain at * least one child task. - * - The {@link MainStage} should only have children if the coordinator is active. - * - The {@link SplitLayout} divider is only visible if both the {@link MainStage} - * and {@link SideStage} are visible. + * - The {@link SplitLayout} divider is only visible if multiple {@link StageTaskListener}s are + * visible * - Both stages are put under a single-top root task. * This rules are mostly implemented in {@link #onStageVisibilityChanged(StageListenerImpl)} and * {@link #onStageHasChildrenChanged(StageListenerImpl).} @@ -178,11 +169,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private static final String TAG = StageCoordinator.class.getSimpleName(); - private final SurfaceSession mSurfaceSession = new SurfaceSession(); - - private final MainStage mMainStage; + private final StageTaskListener mMainStage; private final StageListenerImpl mMainStageListener = new StageListenerImpl(); - private final SideStage mSideStage; + private final StageTaskListener mSideStage; private final StageListenerImpl mSideStageListener = new StageListenerImpl(); @SplitPosition private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT; @@ -191,7 +180,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private SplitLayout mSplitLayout; private ValueAnimator mDividerFadeInAnimator; private boolean mDividerVisible; - private boolean mKeyguardShowing; + private boolean mKeyguardActive; private boolean mShowDecorImmediately; private final SyncTransactionQueue mSyncQueue; private final ShellTaskOrganizer mTaskOrganizer; @@ -205,6 +194,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private SplitScreenTransitions mSplitTransitions; private final SplitscreenEventLogger mLogger; private final ShellExecutor mMainExecutor; + private final Handler mMainHandler; // Cache live tile tasks while entering recents, evict them from stages in finish transaction // if user is opening another task(s). private final ArrayList<Integer> mPausingTasks = new ArrayList<>(); @@ -225,7 +215,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Tracks whether we should update the recent tasks. Only allow this to happen in between enter // and exit, since exit itself can trigger a number of changes that update the stages. - private boolean mShouldUpdateRecents; + private boolean mShouldUpdateRecents = true; private boolean mExitSplitScreenOnHide; private boolean mIsDividerRemoteAnimating; private boolean mIsDropEntering; @@ -233,7 +223,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private boolean mIsExiting; private boolean mIsRootTranslucent; @VisibleForTesting - int mTopStageAfterFoldDismiss; + @StageType int mLastActiveStage; + private boolean mBreakOnNextWake; + /** Used to get the Settings value for "Continue using apps on fold". */ + private FoldLockSettingsObserver mFoldLockSettingsObserver; private DefaultMixedHandler mMixedHandler; private final Toast mSplitUnsupportedToast; @@ -313,9 +306,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, ShellTaskOrganizer taskOrganizer, DisplayController displayController, DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, Transitions transitions, - TransactionPool transactionPool, - IconProvider iconProvider, ShellExecutor mainExecutor, - Optional<RecentTasksController> recentTasks, + TransactionPool transactionPool, IconProvider iconProvider, ShellExecutor mainExecutor, + Handler mainHandler, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, Optional<WindowDecorViewModel> windowDecorViewModel) { mContext = context; @@ -324,6 +316,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mTaskOrganizer = taskOrganizer; mLogger = new SplitscreenEventLogger(); mMainExecutor = mainExecutor; + mMainHandler = mainHandler; mRecentTasks = recentTasks; mLaunchAdjacentController = launchAdjacentController; mWindowDecorViewModel = windowDecorViewModel; @@ -331,22 +324,20 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, taskOrganizer.createRootTask(displayId, WINDOWING_MODE_FULLSCREEN, this /* listener */); ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Creating main/side root task"); - mMainStage = new MainStage( + mMainStage = new StageTaskListener( mContext, mTaskOrganizer, mDisplayId, mMainStageListener, mSyncQueue, - mSurfaceSession, iconProvider, mWindowDecorViewModel); - mSideStage = new SideStage( + mSideStage = new StageTaskListener( mContext, mTaskOrganizer, mDisplayId, mSideStageListener, mSyncQueue, - mSurfaceSession, iconProvider, mWindowDecorViewModel); mDisplayController = displayController; @@ -363,19 +354,18 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, transitions.addHandler(this); mSplitUnsupportedToast = Toast.makeText(mContext, R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT); - // With shell transition, we should update recents tile each callback so set this to true by - // default. - mShouldUpdateRecents = ENABLE_SHELL_TRANSITIONS; + mFoldLockSettingsObserver = new FoldLockSettingsObserver(mainHandler, context); + mFoldLockSettingsObserver.register(); } @VisibleForTesting StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, - ShellTaskOrganizer taskOrganizer, MainStage mainStage, SideStage sideStage, - DisplayController displayController, DisplayImeController displayImeController, + ShellTaskOrganizer taskOrganizer, StageTaskListener mainStage, + StageTaskListener sideStage, DisplayController displayController, + DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, SplitLayout splitLayout, - Transitions transitions, TransactionPool transactionPool, - ShellExecutor mainExecutor, - Optional<RecentTasksController> recentTasks, + Transitions transitions, TransactionPool transactionPool, ShellExecutor mainExecutor, + Handler mainHandler, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, Optional<WindowDecorViewModel> windowDecorViewModel) { mContext = context; @@ -393,6 +383,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, this::onTransitionAnimationComplete, this); mLogger = new SplitscreenEventLogger(); mMainExecutor = mainExecutor; + mMainHandler = mainHandler; mRecentTasks = recentTasks; mLaunchAdjacentController = launchAdjacentController; mWindowDecorViewModel = windowDecorViewModel; @@ -400,6 +391,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, transitions.addHandler(this); mSplitUnsupportedToast = Toast.makeText(mContext, R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT); + mFoldLockSettingsObserver = + new FoldLockSettingsObserver(context.getMainThreadHandler(), context); + mFoldLockSettingsObserver.register(); } public void setMixedHandler(DefaultMixedHandler mixedHandler) { @@ -420,10 +414,23 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return mSideStageListener.mVisible && mMainStageListener.mVisible; } + private void activateSplit(WindowContainerTransaction wct, boolean includingTopTask) { + mMainStage.activate(wct, includingTopTask); + } + public boolean isSplitActive() { return mMainStage.isActive(); } + /** + * Deactivates main stage by removing the stage from the top level split root (usually when a + * task underneath gets removed from the stage root). + * @param reparentToTop whether we want to put the stage root back on top + */ + private void deactivateSplit(WindowContainerTransaction wct, boolean reparentToTop) { + mMainStage.deactivate(wct, reparentToTop); + } + /** @return whether this transition-request has the launch-adjacent flag. */ public boolean requestHasLaunchAdjacentFlag(TransitionRequestInfo request) { final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask(); @@ -480,18 +487,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "moveToStage: task=%d position=%d", task.taskId, stagePosition); prepareEnterSplitScreen(wct, task, stagePosition, false /* resizeAnim */); - if (ENABLE_SHELL_TRANSITIONS) { - mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, - null, this, - isSplitScreenVisible() - ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN, - !mIsDropEntering); - } else { - mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> { - updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); - }); - } + mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, + null, this, + isSplitScreenVisible() + ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN, + !mIsDropEntering); + // Due to drag already pip task entering split by this method so need to reset flag here. mIsDropEntering = false; mSkipEvictingMainStageChildren = false; @@ -502,12 +503,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "removeFromSideStage: task=%d", taskId); final WindowContainerTransaction wct = new WindowContainerTransaction(); - /** - * {@link MainStage} will be deactivated in {@link #onStageHasChildrenChanged} if the - * {@link SideStage} no longer has children. - */ + + // MainStage will be deactivated in onStageHasChildrenChanged() if the other stages + // no longer have children. + final boolean result = mSideStage.removeTask(taskId, - mMainStage.isActive() ? mMainStage.mRootTaskInfo.token : null, + isSplitActive() ? mMainStage.mRootTaskInfo.token : null, wct); mTaskOrganizer.applyTransaction(wct); return result; @@ -592,12 +593,21 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } - /** Use this method to launch an existing Task via a taskId */ - void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) { + /** + * Use this method to launch an existing Task via a taskId. + * @param hideTaskToken If non-null, a task matching this token will be moved to back in the + * same window container transaction as the starting of the intent. + */ + void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options, + @Nullable WindowContainerToken hideTaskToken) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startTask: task=%d position=%d", taskId, position); mSplitRequest = new SplitRequest(taskId, position); final WindowContainerTransaction wct = new WindowContainerTransaction(); options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */); + if (hideTaskToken != null) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Reordering hide-task to bottom"); + wct.reorder(hideTaskToken, false /* onTop */); + } wct.startTask(taskId, options); // If this should be mixed, send the task to avoid split handle transition directly. if (mMixedHandler != null && mMixedHandler.isTaskInPip(taskId, mTaskOrganizer)) { @@ -615,7 +625,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } // If split screen is not activated, we're expecting to open a pair of apps to split. - final int extraTransitType = mMainStage.isActive() + final int extraTransitType = isSplitActive() ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN; prepareEnterSplitScreen(wct, null /* taskInfo */, position, !mIsDropEntering); @@ -623,19 +633,23 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, extraTransitType, !mIsDropEntering); } - /** Launches an activity into split. */ + /** + * Launches an activity into split. + * @param hideTaskToken If non-null, a task matching this token will be moved to back in the + * same window container transaction as the starting of the intent. + */ void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, - @Nullable Bundle options) { + @Nullable Bundle options, @Nullable WindowContainerToken hideTaskToken) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startIntent: intent=%s position=%d", intent.getIntent(), position); mSplitRequest = new SplitRequest(intent.getIntent(), position); - if (!ENABLE_SHELL_TRANSITIONS) { - startIntentLegacy(intent, fillInIntent, position, options); - return; - } final WindowContainerTransaction wct = new WindowContainerTransaction(); options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */); + if (hideTaskToken != null) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Reordering hide-task to bottom"); + wct.reorder(hideTaskToken, false /* onTop */); + } wct.sendPendingIntent(intent, fillInIntent, options); // If this should be mixed, just send the intent to avoid split handle transition directly. @@ -654,7 +668,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } // If split screen is not activated, we're expecting to open a pair of apps to split. - final int extraTransitType = mMainStage.isActive() + final int extraTransitType = isSplitActive() ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN; prepareEnterSplitScreen(wct, null /* taskInfo */, position, !mIsDropEntering); @@ -662,63 +676,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, extraTransitType, !mIsDropEntering); } - /** Launches an activity into split by legacy transition. */ - void startIntentLegacy(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, - @Nullable Bundle options) { - final boolean isEnteringSplit = !isSplitActive(); - - LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() { - @Override - public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, - IRemoteAnimationFinishedCallback finishedCallback, - SurfaceControl.Transaction t) { - if (isEnteringSplit && mSideStage.getChildCount() == 0) { - mMainExecutor.execute(() -> exitSplitScreen( - null /* childrenToTop */, EXIT_REASON_UNKNOWN)); - Log.w(TAG, splitFailureMessage("startIntentLegacy", - "side stage was not populated")); - handleUnsupportedSplitStart(); - } - - if (apps != null) { - for (int i = 0; i < apps.length; ++i) { - if (apps[i].mode == MODE_OPENING) { - t.show(apps[i].leash); - } - } - } - t.apply(); - - if (finishedCallback != null) { - try { - finishedCallback.onAnimationFinished(); - } catch (RemoteException e) { - Slog.e(TAG, "Error finishing legacy transition: ", e); - } - } - - - if (!isEnteringSplit && apps != null) { - final WindowContainerTransaction evictWct = new WindowContainerTransaction(); - prepareEvictNonOpeningChildTasks(position, apps, evictWct); - mSyncQueue.queue(evictWct); - } - } - }; - - final WindowContainerTransaction wct = new WindowContainerTransaction(); - options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, wct); - - // If split still not active, apply windows bounds first to avoid surface reset to - // wrong pos by SurfaceAnimator from wms. - if (isEnteringSplit && mLogger.isEnterRequestedByDrag()) { - updateWindowBounds(mSplitLayout, wct); - } - wct.sendPendingIntent(intent, fillInIntent, options); - mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct); - } - /** Starts 2 tasks in one transition. */ void startTasks(int taskId1, @Nullable Bundle options1, int taskId2, @Nullable Bundle options2, @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, @@ -735,6 +692,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, setSideStagePosition(splitPosition, wct); options1 = options1 != null ? options1 : new Bundle(); addActivityOptions(options1, mSideStage); + prepareTasksForSplitScreen(new int[] {taskId1, taskId2}, wct); wct.startTask(taskId1, options1); startWithTask(wct, taskId2, options2, snapPosition, remoteTransition, instanceId); @@ -765,6 +723,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, options1 = options1 != null ? options1 : new Bundle(); addActivityOptions(options1, mSideStage); wct.sendPendingIntent(pendingIntent, fillInIntent, options1); + prepareTasksForSplitScreen(new int[] {taskId}, wct); startWithTask(wct, taskId, options2, snapPosition, remoteTransition, instanceId); } @@ -808,11 +767,30 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, options1 = options1 != null ? options1 : new Bundle(); addActivityOptions(options1, mSideStage); wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1); + prepareTasksForSplitScreen(new int[] {taskId}, wct); startWithTask(wct, taskId, options2, snapPosition, remoteTransition, instanceId); } /** + * Prepares the tasks whose IDs are provided in `taskIds` for split screen by clearing their + * bounds and windowing mode so that they can inherit the bounds and the windowing mode of + * their root stages. + * + * @param taskIds an array of task IDs whose bounds will be cleared. + * @param wct transaction to clear the bounds on the tasks. + */ + private void prepareTasksForSplitScreen(int[] taskIds, WindowContainerTransaction wct) { + for (int taskId : taskIds) { + ActivityManager.RunningTaskInfo task = mTaskOrganizer.getRunningTaskInfo(taskId); + if (task != null) { + wct.setWindowingMode(task.token, WINDOWING_MODE_UNDEFINED) + .setBounds(task.token, null); + } + } + } + + /** * Starts with the second task to a split pair in one transition. * * @param wct transaction to start the first task @@ -822,10 +800,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void startWithTask(WindowContainerTransaction wct, int mainTaskId, @Nullable Bundle mainOptions, @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { - if (!mMainStage.isActive()) { + if (!isSplitActive()) { // 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(wct, false /* reparent */); + activateSplit(wct, false /* reparentToTop */); } mSplitLayout.setDivideRatio(snapPosition); updateWindowBounds(mSplitLayout, wct); @@ -889,10 +867,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return; } - if (!mMainStage.isActive()) { + if (!isSplitActive()) { // 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(wct, false /* reparent */); + activateSplit(wct, false /* reparentToTop */); } setSideStagePosition(splitPosition, wct); @@ -963,373 +941,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitTransitions.startFullscreenTransition(wct, remoteTransition); } - /** Starts a pair of tasks using legacy transition. */ - void startTasksWithLegacyTransition(int taskId1, @Nullable Bundle options1, - int taskId2, @Nullable Bundle options2, @SplitPosition int splitPosition, - @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter, - InstanceId instanceId) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - if (options1 == null) options1 = new Bundle(); - if (taskId2 == INVALID_TASK_ID) { - // Launching a solo task. - // Exit split first if this task under split roots. - if (mMainStage.containsTask(taskId1) || mSideStage.containsTask(taskId1)) { - exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT); - } - ActivityOptions activityOptions = ActivityOptions.fromBundle(options1); - activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter)); - options1 = activityOptions.toBundle(); - addActivityOptions(options1, null /* launchTarget */); - wct.startTask(taskId1, options1); - mSyncQueue.queue(wct); - return; - } - - addActivityOptions(options1, mSideStage); - wct.startTask(taskId1, options1); - mSplitRequest = new SplitRequest(taskId1, taskId2, splitPosition); - startWithLegacyTransition(wct, taskId2, options2, splitPosition, snapPosition, adapter, - instanceId); - } - - /** Starts a pair of intents using legacy transition. */ - void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, Intent fillInIntent1, - @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1, - @Nullable PendingIntent pendingIntent2, Intent fillInIntent2, - @Nullable ShortcutInfo shortcutInfo2, @Nullable Bundle options2, - @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, - RemoteAnimationAdapter adapter, InstanceId instanceId) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - if (options1 == null) options1 = new Bundle(); - if (pendingIntent2 == null) { - // Launching a solo intent or shortcut as fullscreen. - launchAsFullscreenWithRemoteAnimation(pendingIntent1, fillInIntent1, shortcutInfo1, - options1, adapter, wct); - return; - } - - addActivityOptions(options1, mSideStage); - if (shortcutInfo1 != null) { - wct.startShortcut(mContext.getPackageName(), shortcutInfo1, options1); - } else { - wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1); - mSplitRequest = new SplitRequest(pendingIntent1.getIntent(), - pendingIntent2 != null ? pendingIntent2.getIntent() : null, splitPosition); - } - startWithLegacyTransition(wct, pendingIntent2, fillInIntent2, shortcutInfo2, options2, - splitPosition, snapPosition, adapter, instanceId); - } - - void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, Intent fillInIntent, - @Nullable Bundle options1, int taskId, @Nullable Bundle options2, - @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, - RemoteAnimationAdapter adapter, InstanceId instanceId) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - if (options1 == null) options1 = new Bundle(); - if (taskId == INVALID_TASK_ID) { - // Launching a solo intent as fullscreen. - launchAsFullscreenWithRemoteAnimation(pendingIntent, fillInIntent, null, options1, - adapter, wct); - return; - } - - addActivityOptions(options1, mSideStage); - wct.sendPendingIntent(pendingIntent, fillInIntent, options1); - mSplitRequest = new SplitRequest(taskId, pendingIntent.getIntent(), splitPosition); - startWithLegacyTransition(wct, taskId, options2, splitPosition, snapPosition, adapter, - instanceId); - } - - /** Starts a pair of shortcut and task using legacy transition. */ - void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo, - @Nullable Bundle options1, int taskId, @Nullable Bundle options2, - @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, - RemoteAnimationAdapter adapter, InstanceId instanceId) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - if (options1 == null) options1 = new Bundle(); - if (taskId == INVALID_TASK_ID) { - // Launching a solo shortcut as fullscreen. - launchAsFullscreenWithRemoteAnimation(null, null, shortcutInfo, options1, adapter, wct); - return; - } - - addActivityOptions(options1, mSideStage); - wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1); - startWithLegacyTransition(wct, taskId, options2, splitPosition, snapPosition, adapter, - instanceId); - } - - private void launchAsFullscreenWithRemoteAnimation(@Nullable PendingIntent pendingIntent, - @Nullable Intent fillInIntent, @Nullable ShortcutInfo shortcutInfo, - @Nullable Bundle options, RemoteAnimationAdapter adapter, - WindowContainerTransaction wct) { - LegacyTransitions.ILegacyTransition transition = - (transit, apps, wallpapers, nonApps, finishedCallback, t) -> { - if (apps == null || apps.length == 0) { - onRemoteAnimationFinished(apps); - t.apply(); - try { - adapter.getRunner().onAnimationCancelled(); - } catch (RemoteException e) { - Slog.e(TAG, "Error starting remote animation", e); - } - return; - } - - for (int i = 0; i < apps.length; ++i) { - if (apps[i].mode == MODE_OPENING) { - t.show(apps[i].leash); - } - } - t.apply(); - - try { - adapter.getRunner().onAnimationStart( - transit, apps, wallpapers, nonApps, finishedCallback); - } catch (RemoteException e) { - Slog.e(TAG, "Error starting remote animation", e); - } - }; - - addActivityOptions(options, null /* launchTarget */); - if (shortcutInfo != null) { - wct.startShortcut(mContext.getPackageName(), shortcutInfo, options); - } else if (pendingIntent != null) { - wct.sendPendingIntent(pendingIntent, fillInIntent, options); - } else { - Slog.e(TAG, "Pending intent and shortcut are null is invalid case."); - } - mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct); - } - - private void startWithLegacyTransition(WindowContainerTransaction wct, - @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent, - @Nullable ShortcutInfo mainShortcutInfo, @Nullable Bundle mainOptions, - @SplitPosition int sidePosition, @PersistentSnapPosition int snapPosition, - RemoteAnimationAdapter adapter, InstanceId instanceId) { - startWithLegacyTransition(wct, INVALID_TASK_ID, mainPendingIntent, mainFillInIntent, - mainShortcutInfo, mainOptions, sidePosition, snapPosition, adapter, instanceId); - } - - private void startWithLegacyTransition(WindowContainerTransaction wct, int mainTaskId, - @Nullable Bundle mainOptions, @SplitPosition int sidePosition, - @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter, - InstanceId instanceId) { - startWithLegacyTransition(wct, mainTaskId, null /* mainPendingIntent */, - null /* mainFillInIntent */, null /* mainShortcutInfo */, mainOptions, sidePosition, - snapPosition, adapter, instanceId); - } - - /** - * @param wct transaction to start the first task - * @param instanceId if {@code null}, will not log. Otherwise it will be used in - * {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)} - */ - private void startWithLegacyTransition(WindowContainerTransaction wct, int mainTaskId, - @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent, - @Nullable ShortcutInfo mainShortcutInfo, @Nullable Bundle options, - @SplitPosition int sidePosition, @PersistentSnapPosition int snapPosition, - RemoteAnimationAdapter adapter, InstanceId instanceId) { - if (!isSplitScreenVisible()) { - exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT); - } - - // Init divider first to make divider leash for remote animation target. - mSplitLayout.init(); - mSplitLayout.setDivideRatio(snapPosition); - - // Apply surface bounds before animation start. - SurfaceControl.Transaction startT = mTransactionPool.acquire(); - updateSurfaceBounds(mSplitLayout, startT, false /* applyResizingOffset */); - startT.apply(); - mTransactionPool.release(startT); - - // Set false to avoid record new bounds with old task still on top; - mShouldUpdateRecents = false; - mIsDividerRemoteAnimating = true; - if (mSplitRequest == null) { - mSplitRequest = new SplitRequest(mainTaskId, - mainPendingIntent != null ? mainPendingIntent.getIntent() : null, - sidePosition); - } - setSideStagePosition(sidePosition, wct); - if (!mMainStage.isActive()) { - mMainStage.activate(wct, false /* reparent */); - } - - if (options == null) options = new Bundle(); - addActivityOptions(options, mMainStage); - - updateWindowBounds(mSplitLayout, wct); - wct.reorder(mRootTaskInfo.token, true); - setRootForceTranslucent(false, wct); - - // TODO(b/268008375): Merge APIs to start a split pair into one. - if (mainTaskId != INVALID_TASK_ID) { - options = wrapAsSplitRemoteAnimation(adapter, options); - wct.startTask(mainTaskId, options); - mSyncQueue.queue(wct); - } else { - if (mainShortcutInfo != null) { - wct.startShortcut(mContext.getPackageName(), mainShortcutInfo, options); - } else { - wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, options); - } - mSyncQueue.queue(wrapAsSplitRemoteAnimation(adapter), WindowManager.TRANSIT_OPEN, wct); - } - - setEnterInstanceId(instanceId); - } - - private Bundle wrapAsSplitRemoteAnimation(RemoteAnimationAdapter adapter, Bundle options) { - final WindowContainerTransaction evictWct = new WindowContainerTransaction(); - if (isSplitScreenVisible()) { - mMainStage.evictAllChildren(evictWct); - mSideStage.evictAllChildren(evictWct); - } - - IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() { - @Override - public void onAnimationStart(@WindowManager.TransitionOldType int transit, - RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, - RemoteAnimationTarget[] nonApps, - final IRemoteAnimationFinishedCallback finishedCallback) { - IRemoteAnimationFinishedCallback wrapCallback = - new IRemoteAnimationFinishedCallback.Stub() { - @Override - public void onAnimationFinished() throws RemoteException { - onRemoteAnimationFinishedOrCancelled(evictWct); - finishedCallback.onAnimationFinished(); - } - }; - Transitions.setRunningRemoteTransitionDelegate(adapter.getCallingApplication()); - try { - adapter.getRunner().onAnimationStart(transit, apps, wallpapers, - ArrayUtils.appendElement(RemoteAnimationTarget.class, nonApps, - getDividerBarLegacyTarget()), wrapCallback); - } catch (RemoteException e) { - Slog.e(TAG, "Error starting remote animation", e); - } - } - - @Override - public void onAnimationCancelled() { - onRemoteAnimationFinishedOrCancelled(evictWct); - setDividerVisibility(true, null); - try { - adapter.getRunner().onAnimationCancelled(); - } catch (RemoteException e) { - Slog.e(TAG, "Error starting remote animation", e); - } - } - }; - RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter( - wrapper, adapter.getDuration(), adapter.getStatusBarTransitionDelay()); - ActivityOptions activityOptions = ActivityOptions.fromBundle(options); - activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter)); - return activityOptions.toBundle(); - } - - private LegacyTransitions.ILegacyTransition wrapAsSplitRemoteAnimation( - RemoteAnimationAdapter adapter) { - LegacyTransitions.ILegacyTransition transition = - (transit, apps, wallpapers, nonApps, finishedCallback, t) -> { - if (apps == null || apps.length == 0) { - onRemoteAnimationFinished(apps); - t.apply(); - try { - adapter.getRunner().onAnimationCancelled(); - } catch (RemoteException e) { - Slog.e(TAG, "Error starting remote animation", e); - } - return; - } - - // Wrap the divider bar into non-apps target to animate together. - nonApps = ArrayUtils.appendElement(RemoteAnimationTarget.class, nonApps, - getDividerBarLegacyTarget()); - - for (int i = 0; i < apps.length; ++i) { - if (apps[i].mode == MODE_OPENING) { - t.show(apps[i].leash); - // Reset the surface position of the opening app to prevent offset. - t.setPosition(apps[i].leash, 0, 0); - } - } - setDividerVisibility(true, t); - t.apply(); - - IRemoteAnimationFinishedCallback wrapCallback = - new IRemoteAnimationFinishedCallback.Stub() { - @Override - public void onAnimationFinished() throws RemoteException { - onRemoteAnimationFinished(apps); - finishedCallback.onAnimationFinished(); - } - }; - Transitions.setRunningRemoteTransitionDelegate(adapter.getCallingApplication()); - try { - adapter.getRunner().onAnimationStart( - transit, apps, wallpapers, nonApps, wrapCallback); - } catch (RemoteException e) { - Slog.e(TAG, "Error starting remote animation", e); - } - }; - - return transition; - } - private void setEnterInstanceId(InstanceId instanceId) { if (instanceId != null) { mLogger.enterRequested(instanceId, ENTER_REASON_LAUNCHER); } } - private void onRemoteAnimationFinishedOrCancelled(WindowContainerTransaction evictWct) { - mIsDividerRemoteAnimating = false; - mShouldUpdateRecents = true; - clearRequestIfPresented(); - // If any stage has no child after animation finished, it means that split will display - // nothing, such status will happen if task and intent is same app but not support - // multi-instance, we should exit split and expand that app as full screen. - if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) { - mMainExecutor.execute(() -> - exitSplitScreen(mMainStage.getChildCount() == 0 - ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); - Log.w(TAG, splitFailureMessage("onRemoteAnimationFinishedOrCancelled", - "main or side stage was not populated.")); - handleUnsupportedSplitStart(); - } else { - mSyncQueue.queue(evictWct); - mSyncQueue.runInSync(t -> { - updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); - }); - } - } - - private void onRemoteAnimationFinished(RemoteAnimationTarget[] apps) { - mIsDividerRemoteAnimating = false; - mShouldUpdateRecents = true; - clearRequestIfPresented(); - // If any stage has no child after finished animation, that side of the split will display - // nothing. This might happen if starting the same app on the both sides while not - // supporting multi-instance. Exit the split screen and expand that app to full screen. - if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) { - mMainExecutor.execute(() -> exitSplitScreen(mMainStage.getChildCount() == 0 - ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); - Log.w(TAG, splitFailureMessage("onRemoteAnimationFinished", - "main or side stage was not populated")); - handleUnsupportedSplitStart(); - return; - } - - final WindowContainerTransaction evictWct = new WindowContainerTransaction(); - mMainStage.evictNonOpeningChildren(apps, evictWct); - mSideStage.evictNonOpeningChildren(apps, evictWct); - mSyncQueue.queue(evictWct); - } - void prepareEvictNonOpeningChildTasks(@SplitPosition int position, RemoteAnimationTarget[] apps, WindowContainerTransaction wct) { if (position == mSideStagePosition) { @@ -1422,40 +1039,41 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mTempRect1.setEmpty(); final StageTaskListener topLeftStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; - final SurfaceControl topLeftScreenshot = ScreenshotUtils.takeScreenshot(t, - topLeftStage.mRootLeash, mTempRect1, Integer.MAX_VALUE - 1); final StageTaskListener bottomRightStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; - final SurfaceControl bottomRightScreenshot = ScreenshotUtils.takeScreenshot(t, - bottomRightStage.mRootLeash, mTempRect1, Integer.MAX_VALUE - 1); - mSplitLayout.splitSwitching(t, topLeftStage.mRootLeash, bottomRightStage.mRootLeash, + + // Don't allow windows or divider to be focused during animation (mRootTaskInfo is the + // parent of all 3 leaves). We don't want the user to be able to tap and focus a window + // while it is moving across the screen, because granting focus also recalculates the + // layering order, which is in delicate balance during this animation. + WindowContainerTransaction noFocus = new WindowContainerTransaction(); + noFocus.setFocusable(mRootTaskInfo.token, false); + mSyncQueue.queue(noFocus); + + mSplitLayout.playSwapAnimation(t, topLeftStage, bottomRightStage, insets -> { + // Runs at the end of the swap animation + SplitDecorManager decorManager1 = topLeftStage.getDecorManager(); + SplitDecorManager decorManager2 = bottomRightStage.getDecorManager(); + WindowContainerTransaction wct = new WindowContainerTransaction(); + + // Restore focus-ability to the windows and divider + wct.setFocusable(mRootTaskInfo.token, true); + setSideStagePosition(reverseSplitPosition(mSideStagePosition), wct); mSyncQueue.queue(wct); mSyncQueue.runInSync(st -> { updateSurfaceBounds(mSplitLayout, st, false /* applyResizingOffset */); - st.setPosition(topLeftScreenshot, -insets.left, -insets.top); - st.setPosition(bottomRightScreenshot, insets.left, insets.top); - - final ValueAnimator va = ValueAnimator.ofFloat(1, 0); - va.addUpdateListener(valueAnimator-> { - final float progress = (float) valueAnimator.getAnimatedValue(); - t.setAlpha(topLeftScreenshot, progress); - t.setAlpha(bottomRightScreenshot, progress); - t.apply(); - }); - va.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd( - @androidx.annotation.NonNull Animator animation) { - t.remove(topLeftScreenshot); - t.remove(bottomRightScreenshot); - t.apply(); - mTransactionPool.release(t); - } - }); - va.start(); + + // updateSurfaceBounds(), above, officially puts the two apps in their new + // stages. Starting on the next frame, all calculations are made using the + // new layouts/insets. So any follow-up animations on the same leashes below + // should contain some cleanup/repositioning to prevent jank. + + // Play follow-up animations if needed + decorManager1.fadeOutVeilAndCleanUp(st); + decorManager2.fadeOutVeilAndCleanUp(st); }); }); @@ -1487,83 +1105,88 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } - void onKeyguardVisibilityChanged(boolean showing) { - mKeyguardShowing = showing; - if (!mMainStage.isActive()) { + /** + * Runs when keyguard state changes. The booleans here are a bit complicated, so for reference: + * @param active {@code true} if we are in a state where the keyguard *should* be shown + * -- still true when keyguard is "there" but is behind an app, or + * screen is off. + * @param occludingTaskRunning {@code true} when there is a running task that has + * FLAG_SHOW_WHEN_LOCKED -- also true when the task is + * just running on its own and keyguard is not active + * at all. + */ + void onKeyguardStateChanged(boolean active, boolean occludingTaskRunning) { + mKeyguardActive = active; + if (!isSplitActive()) { return; } - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onKeyguardVisibilityChanged: showing=%b", showing); - setDividerVisibility(!mKeyguardShowing, null); + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, + "onKeyguardVisibilityChanged: active=%b occludingTaskRunning=%b", + active, occludingTaskRunning); + setDividerVisibility(!mKeyguardActive, null); + + if (active && occludingTaskRunning) { + dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); + } } void onFinishedWakingUp() { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onFinishedWakingUp"); - if (!mMainStage.isActive()) { - return; - } - - // Check if there's only one stage visible while keyguard occluded. - final boolean mainStageVisible = mMainStage.mRootTaskInfo.isVisible; - final boolean oneStageVisible = - mMainStage.mRootTaskInfo.isVisible != mSideStage.mRootTaskInfo.isVisible; - if (oneStageVisible && !ENABLE_SHELL_TRANSITIONS) { - // 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 : mSideStage; - exitSplitScreen(toTop, EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); - } - - // Dismiss split if the flag record any side of stages. - if (mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) { - if (ENABLE_SHELL_TRANSITIONS) { - // Need manually clear here due to this transition might be aborted due to keyguard - // on top and lead to no visible change. - clearSplitPairedInRecents(EXIT_REASON_DEVICE_FOLDED); - final WindowContainerTransaction wct = new WindowContainerTransaction(); - prepareExitSplitScreen(mTopStageAfterFoldDismiss, wct); - mSplitTransitions.startDismissTransition(wct, this, - mTopStageAfterFoldDismiss, EXIT_REASON_DEVICE_FOLDED); - setSplitsVisible(false); - } else { - exitSplitScreen( - mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage, - EXIT_REASON_DEVICE_FOLDED); - } - mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; + if (mBreakOnNextWake) { + dismissSplitKeepingLastActiveStage(EXIT_REASON_DEVICE_FOLDED); } } - void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { - mExitSplitScreenOnHide = exitSplitScreenOnHide; + void onStartedGoingToSleep() { + recordLastActiveStage(); } - /** Exits split screen with legacy transition */ - void exitSplitScreen(int toTopTaskId, @ExitReason int exitReason) { - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "exitSplitScreen: topTaskId=%d reason=%s active=%b", - toTopTaskId, exitReasonToString(exitReason), mMainStage.isActive()); - if (!mMainStage.isActive()) return; + /** + * Records the user's last focused stage -- main stage or side stage. Used to determine which + * stage of a split pair should be kept, in cases where system focus has moved elsewhere. + */ + void recordLastActiveStage() { + if (!isSplitActive() || !isSplitScreenVisible()) { + mLastActiveStage = STAGE_TYPE_UNDEFINED; + } else if (mMainStage.isFocused()) { + mLastActiveStage = STAGE_TYPE_MAIN; + } else if (mSideStage.isFocused()) { + mLastActiveStage = STAGE_TYPE_SIDE; + } + } - StageTaskListener childrenToTop = null; - if (mMainStage.containsTask(toTopTaskId)) { - childrenToTop = mMainStage; - } else if (mSideStage.containsTask(toTopTaskId)) { - childrenToTop = mSideStage; + /** + * Dismisses split, keeping the app that the user focused last in split screen. If the user was + * not in split screen, {@link #mLastActiveStage} should be set to STAGE_TYPE_UNDEFINED, and we + * will do a no-op. + */ + void dismissSplitKeepingLastActiveStage(@ExitReason int reason) { + if (!isSplitActive() || mLastActiveStage == STAGE_TYPE_UNDEFINED) { + // no-op + return; } + // Need manually clear here due to this transition might be aborted due to keyguard + // on top and lead to no visible change. + clearSplitPairedInRecents(reason); final WindowContainerTransaction wct = new WindowContainerTransaction(); - if (childrenToTop != null) { - childrenToTop.reorderChild(toTopTaskId, true /* onTop */, wct); - } - applyExitSplitScreen(childrenToTop, wct, exitReason); + prepareExitSplitScreen(mLastActiveStage, wct); + mSplitTransitions.startDismissTransition(wct, this, mLastActiveStage, reason); + setSplitsVisible(false); + mBreakOnNextWake = false; + logExit(reason); + } + + void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { + mExitSplitScreenOnHide = exitSplitScreenOnHide; } /** Exits split screen with legacy transition */ private void exitSplitScreen(@Nullable StageTaskListener childrenToTop, @ExitReason int exitReason) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "exitSplitScreen: mainStageToTop=%b reason=%s active=%b", - childrenToTop == mMainStage, exitReasonToString(exitReason), mMainStage.isActive()); - if (!mMainStage.isActive()) return; + childrenToTop == mMainStage, exitReasonToString(exitReason), isSplitActive()); + if (!isSplitActive()) return; final WindowContainerTransaction wct = new WindowContainerTransaction(); applyExitSplitScreen(childrenToTop, wct, exitReason); @@ -1573,7 +1196,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, WindowContainerTransaction wct, @ExitReason int exitReason) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "applyExitSplitScreen: reason=%s", exitReasonToString(exitReason)); - if (!mMainStage.isActive() || mIsExiting) return; + if (!isSplitActive() || mIsExiting) return; onSplitScreenExit(); clearSplitPairedInRecents(exitReason); @@ -1585,7 +1208,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.getInvisibleBounds(mTempRect1); if (childrenToTop == null || childrenToTop.getTopVisibleChildTaskId() == INVALID_TASK_ID) { mSideStage.removeAllTasks(wct, false /* toTop */); - mMainStage.deactivate(wct, false /* toTop */); + deactivateSplit(wct, false /* reparentToTop */); wct.reorder(mRootTaskInfo.token, false /* onTop */); setRootForceTranslucent(true, wct); wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); @@ -1614,7 +1237,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, childrenToTop.fadeOutDecor(() -> { WindowContainerTransaction finishedWCT = new WindowContainerTransaction(); mIsExiting = false; - mMainStage.deactivate(finishedWCT, childrenToTop == mMainStage /* toTop */); + deactivateSplit(finishedWCT, childrenToTop == mMainStage /* reparentToTop */); mSideStage.removeAllTasks(finishedWCT, childrenToTop == mSideStage /* toTop */); finishedWCT.reorder(mRootTaskInfo.token, false /* toTop */); setRootForceTranslucent(true, finishedWCT); @@ -1637,11 +1260,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } void dismissSplitScreen(int toTopTaskId, @ExitReason int exitReason) { - if (!mMainStage.isActive()) return; + if (!isSplitActive()) return; final int stage = getStageOfTask(toTopTaskId); final WindowContainerTransaction wct = new WindowContainerTransaction(); prepareExitSplitScreen(stage, wct); mSplitTransitions.startDismissTransition(wct, this, stage, exitReason); + logExit(exitReason); } /** @@ -1738,6 +1362,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage.doForAllChildTasks(taskId -> recentTasks.removeSplitPair(taskId)); mSideStage.doForAllChildTasks(taskId -> recentTasks.removeSplitPair(taskId)); }); + logExit(exitReason); } /** @@ -1747,10 +1372,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, */ void prepareExitSplitScreen(@StageType int stageToTop, @NonNull WindowContainerTransaction wct) { - if (!mMainStage.isActive()) return; + if (!isSplitActive()) return; ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%d", stageToTop); mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE); - mMainStage.deactivate(wct, stageToTop == STAGE_TYPE_MAIN); + deactivateSplit(wct, stageToTop == STAGE_TYPE_MAIN); } private void prepareEnterSplitScreen(WindowContainerTransaction wct) { @@ -1808,20 +1433,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, boolean resizeAnim) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareActiveSplit: task=%d isSplitVisible=%b", taskInfo != null ? taskInfo.taskId : -1, isSplitScreenVisible()); - if (!ENABLE_SHELL_TRANSITIONS) { - // Legacy transition we need to create divider here, shell transition case we will - // create it on #finishEnterSplitScreen - mSplitLayout.init(); - } else { - // We handle split visibility itself on shell transition, but sometimes we didn't - // reset it correctly after dismiss by some reason, so just set invisible before active. - setSplitsVisible(false); - } + // We handle split visibility itself on shell transition, but sometimes we didn't + // reset it correctly after dismiss by some reason, so just set invisible before active. + setSplitsVisible(false); if (taskInfo != null) { setSideStagePosition(startPosition, wct); mSideStage.addTask(taskInfo, wct); } - mMainStage.activate(wct, true /* includingTopTask */); + activateSplit(wct, true /* reparentToTop */); prepareSplitLayout(wct, resizeAnim); } @@ -1862,12 +1481,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSkipEvictingMainStageChildren = false; mSplitRequest = null; updateRecentTasksSplitPair(); - if (!mLogger.hasStartedSession()) { - mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(), - getMainStagePosition(), mMainStage.getTopChildTaskUid(), - getSideStagePosition(), mSideStage.getTopChildTaskUid(), - mSplitLayout.isLeftRightSplit()); - } + + mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(), + getMainStagePosition(), mMainStage.getTopChildTaskUid(), + getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLeftRightSplit()); } void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) { @@ -1892,8 +1510,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } // Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split // will be canceled. - options.setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + options.setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); // TODO (b/336477473): Disallow enter PiP when launching a task in split by default; // this might have to be changed as more split-to-pip cujs are defined. @@ -1963,7 +1581,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (stage == STAGE_TYPE_MAIN) { mLogger.logMainStageAppChange(getMainStagePosition(), mMainStage.getTopChildTaskUid(), mSplitLayout.isLeftRightSplit()); - } else { + } else if (stage == STAGE_TYPE_SIDE) { mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(), mSplitLayout.isLeftRightSplit()); } @@ -2036,7 +1654,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout = new SplitLayout(TAG + "SplitDivider", mContext, mRootTaskInfo.configuration, this, mParentContainerCallbacks, mDisplayController, mDisplayImeController, mTaskOrganizer, - PARALLAX_ALIGN_CENTER /* parallaxType */); + PARALLAX_ALIGN_CENTER /* parallaxType */, mMainHandler); mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout); } @@ -2049,11 +1667,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (mRootTaskInfo == null || mRootTaskInfo.taskId != taskInfo.taskId) { throw new IllegalArgumentException(this + "\n Unknown task info changed: " + taskInfo); } - mWindowDecorViewModel.ifPresent(viewModel -> viewModel.onTaskInfoChanged(taskInfo)); mRootTaskInfo = taskInfo; if (mSplitLayout != null && mSplitLayout.updateConfiguration(mRootTaskInfo.configuration) - && mMainStage.isActive()) { + && isSplitActive()) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskInfoChanged: task=%d updating", taskInfo.taskId); // Clear the divider remote animating flag as the divider will be re-rendered to apply @@ -2206,11 +1823,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "setDividerVisibility: visible=%b keyguardShowing=%b dividerAnimating=%b caller=%s", - visible, mKeyguardShowing, mIsDividerRemoteAnimating, Debug.getCaller()); + visible, mKeyguardActive, mIsDividerRemoteAnimating, Debug.getCaller()); // Defer showing divider bar after keyguard dismissed, so it won't interfere with keyguard // dismissing animation. - if (visible && mKeyguardShowing) { + if (visible && mKeyguardActive) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, " Defer showing divider bar due to keyguard showing."); return; @@ -2307,7 +1924,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, stageListener == mMainStageListener); final boolean hasChildren = stageListener.mHasChildren; final boolean isSideStage = stageListener == mSideStageListener; - if (!hasChildren && !mIsExiting && mMainStage.isActive()) { + if (!hasChildren && !mIsExiting && isSplitActive()) { if (isSideStage && mMainStageListener.mVisible) { // Exit to main stage if side stage no longer has children. mSplitLayout.flingDividerToDismiss( @@ -2322,7 +1939,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Dismiss split screen in the background once any sides of the split become empty. exitSplitScreen(null /* childrenToTop */, EXIT_REASON_APP_FINISHED); } - } else if (isSideStage && hasChildren && !mMainStage.isActive()) { + } else if (isSideStage && hasChildren && !isSplitActive()) { final WindowContainerTransaction wct = new WindowContainerTransaction(); prepareEnterSplitScreen(wct); @@ -2343,15 +1960,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, clearRequestIfPresented(); updateRecentTasksSplitPair(); - if (!mLogger.hasStartedSession()) { - if (!mLogger.hasValidEnterSessionId()) { - mLogger.enterRequested(null /*enterSessionId*/, ENTER_REASON_MULTI_INSTANCE); - } - mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(), - getMainStagePosition(), mMainStage.getTopChildTaskUid(), - getSideStagePosition(), mSideStage.getTopChildTaskUid(), - mSplitLayout.isLeftRightSplit()); + if (!mLogger.hasStartedSession() && !mLogger.hasValidEnterSessionId()) { + mLogger.enterRequested(null /*enterSessionId*/, ENTER_REASON_MULTI_INSTANCE); } + mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(), + getMainStagePosition(), mMainStage.getTopChildTaskUid(), + getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLeftRightSplit()); } } @@ -2363,10 +1978,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, bottomOrRight ? mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT : mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT; final StageTaskListener toTopStage = mainStageToTop ? mMainStage : mSideStage; - if (!ENABLE_SHELL_TRANSITIONS) { - exitSplitScreen(toTopStage, exitReason); - return; - } final int dismissTop = mainStageToTop ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; final WindowContainerTransaction wct = new WindowContainerTransaction(); @@ -2400,13 +2011,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, updateSurfaceBounds(layout, t, shouldUseParallaxEffect); getMainStageBounds(mTempRect1); getSideStageBounds(mTempRect2); - // TODO (b/307490004): "commonColor" below is a temporary fix to ensure the colors on both - // sides match. When b/307490004 is fixed, this code can be reverted. - float[] commonColor = getResizingBackgroundColor(mSideStage.mRootTaskInfo).getComponents(); - mMainStage.onResizing( - mTempRect1, mTempRect2, t, offsetX, offsetY, mShowDecorImmediately, commonColor); - mSideStage.onResizing( - mTempRect2, mTempRect1, t, offsetX, offsetY, mShowDecorImmediately, commonColor); + mMainStage.onResizing(mTempRect1, mTempRect2, t, offsetX, offsetY, mShowDecorImmediately); + mSideStage.onResizing(mTempRect2, mTempRect1, t, offsetX, offsetY, mShowDecorImmediately); t.apply(); mTransactionPool.release(t); } @@ -2429,28 +2035,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } sendOnBoundsChanged(); - if (ENABLE_SHELL_TRANSITIONS) { - mSplitLayout.setDividerInteractive(false, false, "onSplitResizeStart"); - mSplitTransitions.startResizeTransition(wct, this, (aborted) -> { - mSplitLayout.setDividerInteractive(true, false, "onSplitResizeConsumed"); - }, (finishWct, t) -> { - mSplitLayout.setDividerInteractive(true, false, "onSplitResizeFinish"); - }, mMainStage.getSplitDecorManager(), mSideStage.getSplitDecorManager()); - } else { - // Only need screenshot for legacy case because shell transition should screenshot - // itself during transition. - final SurfaceControl.Transaction startT = mTransactionPool.acquire(); - mMainStage.screenshotIfNeeded(startT); - mSideStage.screenshotIfNeeded(startT); - mTransactionPool.release(startT); + mSplitLayout.setDividerInteractive(false, false, "onSplitResizeStart"); + mSplitTransitions.startResizeTransition(wct, this, (aborted) -> { + mSplitLayout.setDividerInteractive(true, false, "onSplitResizeConsumed"); + }, (finishWct, t) -> { + mSplitLayout.setDividerInteractive(true, false, "onSplitResizeFinish"); + }, mMainStage.getSplitDecorManager(), mSideStage.getSplitDecorManager()); - mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> { - updateSurfaceBounds(layout, t, false /* applyResizingOffset */); - mMainStage.onResized(t); - mSideStage.onResized(t); - }); - } mLogger.logResize(mSplitLayout.getDividerPositionAsFraction()); } @@ -2561,7 +2152,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void onDisplayChange(int displayId, int fromRotation, int toRotation, @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) { - if (displayId != DEFAULT_DISPLAY || !mMainStage.isActive()) { + if (displayId != DEFAULT_DISPLAY || !isSplitActive()) { return; } @@ -2580,21 +2171,24 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @VisibleForTesting void onFoldedStateChanged(boolean folded) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onFoldedStateChanged: folded=%b", folded); - mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; - if (!folded) return; - - if (!isSplitActive() || !isSplitScreenVisible()) return; - // To avoid split dismiss when user fold the device and unfold to use later, we only - // record the flag here and try to dismiss on wakeUp callback to ensure split dismiss - // when user interact on phone folded. - if (mMainStage.isFocused()) { - mTopStageAfterFoldDismiss = STAGE_TYPE_MAIN; - } else if (mSideStage.isFocused()) { - mTopStageAfterFoldDismiss = STAGE_TYPE_SIDE; + if (folded) { + recordLastActiveStage(); + // If user folds and has the setting "Continue using apps on fold = NEVER", we assume + // they don't want to continue using split on the outer screen (i.e. we break split if + // they wake the device in its folded state). + mBreakOnNextWake = willSleepOnFold(); + } else { + mBreakOnNextWake = false; } } + /** Returns true if the phone will sleep when it folds. */ + @VisibleForTesting + boolean willSleepOnFold() { + return mFoldLockSettingsObserver != null && mFoldLockSettingsObserver.isSleepOnFold(); + } + private Rect getSideStageBounds() { return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSplitLayout.getBounds1() : mSplitLayout.getBounds2(); @@ -2677,10 +2271,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final @WindowManager.TransitionType int type = request.getType(); final boolean isOpening = isOpeningType(type); final boolean inFullscreen = triggerTask.getWindowingMode() == WINDOWING_MODE_FULLSCREEN; + final StageTaskListener stage = getStageOfTask(triggerTask); if (isOpening && inFullscreen) { // One task is opening into fullscreen mode, remove the corresponding split record. mRecentTasks.ifPresent(recentTasks -> recentTasks.removeSplitPair(triggerTask.taskId)); + logExit(EXIT_REASON_FULLSCREEN_REQUEST); } if (isSplitActive()) { @@ -2692,7 +2288,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, + " sideChildren=%d", triggerTask.taskId, transitTypeToString(type), mMainStage.getChildCount(), mSideStage.getChildCount()); out = new WindowContainerTransaction(); - final StageTaskListener stage = getStageOfTask(triggerTask); if (stage != null) { if (isClosingType(type) && stage.getChildCount() == 1) { // Dismiss split if the last task in one of the stages is going away @@ -2765,16 +2360,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Don't intercept the transition if we are not handling it as a part of one of the // cases above and it is not already visible return null; - } else { - if (triggerTask.parentTaskId == mMainStage.mRootTaskInfo.taskId - || triggerTask.parentTaskId == mSideStage.mRootTaskInfo.taskId) { - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d " - + "restoring to split", request.getDebugId()); - out = new WindowContainerTransaction(); - mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(), - TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, false /* resizeAnim */); - } - if (isOpening && getStageOfTask(triggerTask) != null) { + } else if (stage != null) { + if (isOpening) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d enter split", request.getDebugId()); // One task is appearing into split, prepare to enter split screen. @@ -2782,9 +2369,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareEnterSplitScreen(out); mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(), TRANSIT_SPLIT_SCREEN_PAIR_OPEN, !mIsDropEntering); + return out; } - return out; + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d " + + "restoring to split", request.getDebugId()); + out = new WindowContainerTransaction(); + mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(), + TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, false /* resizeAnim */); } + return out; } /** @@ -2811,6 +2404,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (triggerTask != null) { mRecentTasks.ifPresent( recentTasks -> recentTasks.removeSplitPair(triggerTask.taskId)); + logExit(EXIT_REASON_CHILD_TASK_ENTER_PIP); } @StageType int topStage = STAGE_TYPE_UNDEFINED; if (isSplitScreenVisible()) { @@ -2855,13 +2449,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Not entering or exiting, so just do some house-keeping and validation. // If we're not in split-mode, just abort so something else can handle it. - if (!mMainStage.isActive()) return false; + if (!isSplitActive()) return false; ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startAnimation: transition=%d", info.getDebugId()); mSplitLayout.setFreezeDividerWindow(false); final StageChangeRecord record = new StageChangeRecord(); final int transitType = info.getType(); TransitionInfo.Change pipChange = null; + int closingSplitTaskId = -1; for (int iC = 0; iC < info.getChanges().size(); ++iC) { final TransitionInfo.Change change = info.getChanges().get(iC); if (change.getMode() == TRANSIT_CHANGE @@ -2923,21 +2518,31 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, + " with " + taskInfo.taskId + " before startAnimation()."); } } + if (isClosingType(change.getMode()) && + getStageOfTask(change.getTaskInfo().taskId) != STAGE_TYPE_UNDEFINED) { + // If either one of the 2 stages is closing we're assuming we'll break split + closingSplitTaskId = change.getTaskInfo().taskId; + } } if (pipChange != null) { TransitionInfo.Change pipReplacingChange = getPipReplacingChange(info, pipChange, mMainStage.mRootTaskInfo.taskId, mSideStage.mRootTaskInfo.taskId, getSplitItemStage(pipChange.getLastParent())); - if (pipReplacingChange != null) { + boolean keepSplitWithPip = pipReplacingChange != null && closingSplitTaskId == -1; + if (keepSplitWithPip) { // Set an enter transition for when startAnimation gets called again mSplitTransitions.setEnterTransition(transition, /*remoteTransition*/ null, TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, /*resizeAnim*/ false); + } else { + int finalClosingTaskId = closingSplitTaskId; + mRecentTasks.ifPresent(recentTasks -> + recentTasks.removeSplitPair(finalClosingTaskId)); + logExit(EXIT_REASON_FULLSCREEN_REQUEST); } mMixedHandler.animatePendingEnterPipFromSplit(transition, info, - startTransaction, finishTransaction, finishCallback, - pipReplacingChange != null); + startTransaction, finishTransaction, finishCallback, keepSplitWithPip); notifySplitAnimationFinished(); return true; } @@ -3098,7 +2703,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, public void onTransitionAnimationComplete() { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTransitionAnimationComplete"); // If still playing, let it finish. - if (!mMainStage.isActive() && !mIsExiting) { + if (!isSplitActive() && !mIsExiting) { // Update divider state after animation so that it is still around and positioned // properly for the animation itself. mSplitLayout.release(); @@ -3153,7 +2758,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final int dismissTop = mainChild != null ? STAGE_TYPE_MAIN : (sideChild != null ? STAGE_TYPE_SIDE : STAGE_TYPE_UNDEFINED); pendingEnter.cancel( - (cancelWct, cancelT) -> prepareExitSplitScreen(dismissTop, cancelWct)); + (cancelWct, cancelT) -> { + prepareExitSplitScreen(dismissTop, cancelWct); + logExit(EXIT_REASON_UNKNOWN); + }); Log.w(TAG, splitFailureMessage("startPendingEnterAnimation", "launched 2 tasks in split, but didn't receive " + "2 tasks in transition. Possibly one of them failed to launch")); @@ -3224,6 +2832,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.flingDividerToCenter(this::notifySplitAnimationFinished); } callbackWct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false); + mWindowDecorViewModel.ifPresent(viewModel -> { + if (finalMainChild != null) { + viewModel.onTaskInfoChanged(finalMainChild.getTaskInfo()); + } + if (finalSideChild != null) { + viewModel.onTaskInfoChanged(finalSideChild.getTaskInfo()); + } + }); mPausingTasks.clear(); }); @@ -3549,16 +3165,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, info.addChange(barChange); } - 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); - } - @NeverCompile @Override public void dump(@NonNull PrintWriter pw, String prefix) { @@ -3568,10 +3174,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, pw.println(innerPrefix + "mDividerVisible=" + mDividerVisible); pw.println(innerPrefix + "isSplitActive=" + isSplitActive()); pw.println(innerPrefix + "isSplitVisible=" + isSplitScreenVisible()); - pw.println(innerPrefix + "isLeftRightSplit=" + mSplitLayout.isLeftRightSplit()); + pw.println(innerPrefix + "isLeftRightSplit=" + + (mSplitLayout != null ? mSplitLayout.isLeftRightSplit() : "null")); pw.println(innerPrefix + "MainStage"); pw.println(childPrefix + "stagePosition=" + splitPositionToString(getMainStagePosition())); - pw.println(childPrefix + "isActive=" + mMainStage.isActive()); + pw.println(childPrefix + "isActive=" + isSplitActive()); mMainStage.dump(pw, childPrefix); pw.println(innerPrefix + "MainStageListener"); mMainStageListener.dump(pw, childPrefix); @@ -3580,7 +3187,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStage.dump(pw, childPrefix); pw.println(innerPrefix + "SideStageListener"); mSideStageListener.dump(pw, childPrefix); - mSplitLayout.dump(pw, childPrefix); + if (mSplitLayout != null) { + mSplitLayout.dump(pw, childPrefix); + } if (!mPausingTasks.isEmpty()) { pw.println(childPrefix + "mPausingTasks=" + mPausingTasks); } @@ -3606,30 +3215,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mIsDropEntering = true; mSkipEvictingMainStageChildren = true; } - if (!isSplitScreenVisible() && !ENABLE_SHELL_TRANSITIONS) { - // If split running background, exit split first. - // Skip this on shell transition due to we could evict existing tasks on transition - // finished. - exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT); - } mLogger.enterRequestedByDrag(position, dragSessionId); } /** - * Sets info to be logged when splitscreen is next entered. - */ - public void onRequestToSplit(InstanceId sessionId, @SplitEnterReason int enterReason) { - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRequestToSplit: reason=%d", enterReason); - if (!isSplitScreenVisible() && !ENABLE_SHELL_TRANSITIONS) { - // If split running background, exit split first. - // Skip this on shell transition due to we could evict existing tasks on transition - // finished. - exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT); - } - mLogger.enterRequested(sessionId, enterReason); - } - - /** * Logs the exit of splitscreen. */ private void logExit(@ExitReason int exitReason) { @@ -3709,14 +3298,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public void onNoLongerSupportMultiWindow(ActivityManager.RunningTaskInfo taskInfo) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onNoLongerSupportMultiWindow: task=%s", taskInfo); - if (mMainStage.isActive()) { + if (isSplitActive()) { final boolean isMainStage = mMainStageListener == this; - if (!ENABLE_SHELL_TRANSITIONS) { - StageCoordinator.this.exitSplitScreen(isMainStage ? mMainStage : mSideStage, - EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); - handleUnsupportedSplitStart(); - return; - } // If visible, we preserve the app and keep it running. If an app becomes // unsupported in the bg, break split without putting anything on top @@ -3731,8 +3314,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitTransitions.startDismissTransition(wct, StageCoordinator.this, stageType, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); Log.w(TAG, splitFailureMessage("onNoLongerSupportMultiWindow", - "app package " + taskInfo.baseActivity.getPackageName() - + " does not support splitscreen, or is a controlled activity type")); + "app package " + taskInfo.baseIntent.getComponent() + + " does not support splitscreen, or is a controlled activity" + + " type")); if (splitScreenVisible) { handleUnsupportedSplitStart(); } 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 0f3d6cade95a..d64c0a24be68 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 @@ -22,9 +22,9 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED; import static android.view.RemoteAnimationTarget.MODE_OPENING; -import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES; -import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES; -import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE; +import static com.android.wm.shell.shared.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES; +import static com.android.wm.shell.shared.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES; +import static com.android.wm.shell.shared.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; @@ -39,13 +39,12 @@ import android.util.Slog; import android.util.SparseArray; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.internal.util.ArrayUtils; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTaskOrganizer; @@ -69,9 +68,13 @@ import java.util.function.Predicate; * * @see StageCoordinator */ -class StageTaskListener implements ShellTaskOrganizer.TaskListener { +public class StageTaskListener implements ShellTaskOrganizer.TaskListener { private static final String TAG = StageTaskListener.class.getSimpleName(); + // No current way to enforce this but if enableFlexibleSplit() is enabled, then only 1 of the + // stages should have this be set/being used + private boolean mIsActive; + /** Callback interface for listening to changes in a split-screen stage. */ public interface StageListenerCallbacks { void onRootTaskAppeared(); @@ -89,7 +92,6 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { private final Context mContext; private final StageListenerCallbacks mCallbacks; - private final SurfaceSession mSurfaceSession; private final SyncTransactionQueue mSyncQueue; private final IconProvider mIconProvider; private final Optional<WindowDecorViewModel> mWindowDecorViewModel; @@ -104,12 +106,11 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { StageTaskListener(Context context, ShellTaskOrganizer taskOrganizer, int displayId, StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, - SurfaceSession surfaceSession, IconProvider iconProvider, + IconProvider iconProvider, Optional<WindowDecorViewModel> windowDecorViewModel) { mContext = context; mCallbacks = callbacks; mSyncQueue = syncQueue; - mSurfaceSession = surfaceSession; mIconProvider = iconProvider; mWindowDecorViewModel = windowDecorViewModel; taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this); @@ -162,6 +163,18 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { return getChildTaskInfo(predicate) != null; } + public SurfaceControl getRootLeash() { + return mRootLeash; + } + + public ActivityManager.RunningTaskInfo getRunningTaskInfo() { + return mRootTaskInfo; + } + + public SplitDecorManager getDecorManager() { + return mSplitDecorManager; + } + @Nullable private ActivityManager.RunningTaskInfo getChildTaskInfo( Predicate<ActivityManager.RunningTaskInfo> predicate) { @@ -187,12 +200,11 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { mRootTaskInfo = taskInfo; mSplitDecorManager = new SplitDecorManager( mRootTaskInfo.configuration, - mIconProvider, - mSurfaceSession); + mIconProvider); mCallbacks.onRootTaskAppeared(); sendStatusChanged(); mSyncQueue.runInSync(t -> mDimLayer = - SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer", mSurfaceSession)); + SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer")); } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { final int taskId = taskInfo.taskId; mChildrenLeashes.put(taskId, leash); @@ -314,10 +326,10 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } void onResizing(Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t, int offsetX, - int offsetY, boolean immediately, float[] veilColor) { + int offsetY, boolean immediately) { if (mSplitDecorManager != null && mRootTaskInfo != null) { mSplitDecorManager.onResizing(mRootTaskInfo, newBounds, sideBounds, t, offsetX, - offsetY, immediately, veilColor); + offsetY, immediately); } } @@ -335,7 +347,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { void fadeOutDecor(Runnable finishedCallback) { if (mSplitDecorManager != null) { - mSplitDecorManager.fadeOutDecor(finishedCallback); + mSplitDecorManager.fadeOutDecor(finishedCallback, false /* addDelay */); } else { finishedCallback.run(); } @@ -463,6 +475,68 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { }); } + // --------- + // Previously only used in MainStage + boolean isActive() { + return mIsActive; + } + + void activate(WindowContainerTransaction wct, boolean includingTopTask) { + if (mIsActive) return; + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "activate: includingTopTask=%b", + includingTopTask); + + if (includingTopTask) { + reparentTopTask(wct); + } + + mIsActive = true; + } + + void deactivate(WindowContainerTransaction wct) { + deactivate(wct, false /* toTop */); + } + + void deactivate(WindowContainerTransaction wct, boolean toTop) { + if (!mIsActive) return; + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "deactivate: toTop=%b rootTaskInfo=%s", + toTop, mRootTaskInfo); + mIsActive = false; + + if (mRootTaskInfo == null) return; + final WindowContainerToken rootToken = mRootTaskInfo.token; + wct.reparentTasks( + rootToken, + null /* newParent */, + null /* windowingModes */, + null /* activityTypes */, + toTop); + } + + // -------- + // Previously only used in SideStage + boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "remove all side stage tasks: childCount=%d toTop=%b", + mChildrenTaskInfo.size(), toTop); + if (mChildrenTaskInfo.size() == 0) return false; + wct.reparentTasks( + mRootTaskInfo.token, + null /* newParent */, + null /* windowingModes */, + null /* activityTypes */, + toTop); + return true; + } + + boolean removeTask(int taskId, WindowContainerToken newParent, WindowContainerTransaction wct) { + final ActivityManager.RunningTaskInfo task = mChildrenTaskInfo.get(taskId); + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "remove side stage task: task=%d exists=%b", taskId, + task != null); + if (task == null) return false; + wct.reparent(task.token, newParent, false /* onTop */); + return true; + } + private void sendStatusChanged() { mCallbacks.onStatusChanged(mRootTaskInfo.isVisible, mChildrenTaskInfo.size() > 0); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuController.java index 1d8a8d506c5c..bb2f60b64a4a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuController.java @@ -36,11 +36,11 @@ import android.view.LayoutInflater; import android.view.WindowManager; import android.view.WindowManagerGlobal; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.SystemWindows; -import com.android.wm.shell.common.split.SplitScreenConstants; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.split.SplitScreenConstants; /** * Handles the interaction logic with the {@link TvSplitMenuView}. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuView.java index 88e9757a9b31..b758b531075a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitMenuView.java @@ -19,8 +19,8 @@ package com.android.wm.shell.splitscreen.tv; import static android.view.KeyEvent.ACTION_DOWN; import static android.view.KeyEvent.KEYCODE_BACK; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import android.content.Context; import android.util.AttributeSet; @@ -31,7 +31,7 @@ import android.widget.LinearLayout; import androidx.annotation.Nullable; import com.android.wm.shell.R; -import com.android.wm.shell.common.split.SplitScreenConstants; +import com.android.wm.shell.shared.split.SplitScreenConstants; /** * A View for the Menu Window. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java index e330f3ab65ab..34681569a16c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java @@ -32,9 +32,8 @@ import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; -import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.splitscreen.StageCoordinator; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -88,7 +87,8 @@ public class TvSplitScreenController extends SplitScreenController { syncQueue, rootTDAOrganizer, displayController, displayImeController, displayInsetsController, null, transitions, transactionPool, iconProvider, recentTasks, launchAdjacentController, Optional.empty(), - Optional.empty(), null /* stageCoordinator */, multiInstanceHelper, mainExecutor); + Optional.empty(), null /* stageCoordinator */, multiInstanceHelper, mainExecutor, + mainHandler); mTaskOrganizer = shellTaskOrganizer; mSyncQueue = syncQueue; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java index 79476919221e..4451ee887363 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java @@ -28,9 +28,9 @@ import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; -import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.common.split.SplitScreenConstants; import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.shared.TransactionPool; +import com.android.wm.shell.shared.split.SplitScreenConstants; import com.android.wm.shell.splitscreen.StageCoordinator; import com.android.wm.shell.transition.Transitions; @@ -56,7 +56,7 @@ public class TvStageCoordinator extends StageCoordinator SystemWindows systemWindows) { super(context, displayId, syncQueue, taskOrganizer, displayController, displayImeController, displayInsetsController, transitions, transactionPool, iconProvider, - mainExecutor, recentTasks, launchAdjacentController, Optional.empty()); + mainExecutor, mainHandler, recentTasks, launchAdjacentController, Optional.empty()); mTvSplitMenuController = new TvSplitMenuController(context, this, systemWindows, mainHandler); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java index edb5aba1e46b..42b8b73cfb80 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java @@ -29,7 +29,8 @@ import android.window.SplashScreenView; import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.R; -import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.shared.TransactionPool; +import com.android.wm.shell.shared.startingsurface.SplashScreenExitAnimationUtils; /** * Default animation for exiting the splash screen window. 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 2b12a22f907d..81f444ba2af3 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 @@ -17,6 +17,7 @@ package com.android.wm.shell.startingsurface; import static android.content.Context.CONTEXT_RESTRICTED; +import static android.content.res.Configuration.UI_MODE_NIGHT_MASK; import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.Display.DEFAULT_DISPLAY; @@ -74,11 +75,11 @@ import com.android.internal.graphics.palette.Palette; import com.android.internal.graphics.palette.Quantizer; import com.android.internal.graphics.palette.VariationalKMeansQuantizer; import com.android.internal.policy.PhoneWindow; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.BaseIconFactory; import com.android.launcher3.icons.IconProvider; -import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.TransactionPool; import java.util.List; import java.util.function.Consumer; @@ -191,31 +192,15 @@ public class SplashscreenContentDrawer { } final Configuration taskConfig = taskInfo.getConfiguration(); - if (taskConfig.diffPublicOnly(context.getResources().getConfiguration()) != 0) { + final Configuration contextConfig = context.getResources().getConfiguration(); + if ((taskConfig.uiMode & UI_MODE_NIGHT_MASK) + != (contextConfig.uiMode & UI_MODE_NIGHT_MASK)) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, "addSplashScreen: creating context based on task Configuration %s", taskConfig); final Context overrideContext = context.createConfigurationContext(taskConfig); overrideContext.setTheme(theme); - final TypedArray typedArray = overrideContext.obtainStyledAttributes( - com.android.internal.R.styleable.Window); - final int resId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0); - try { - if (resId != 0 && overrideContext.getDrawable(resId) != null) { - // We want to use the windowBackground for the override context if it is - // available, otherwise we use the default one to make sure a themed starting - // window is displayed for the app. - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW, - "addSplashScreen: apply overrideConfig %s", - taskConfig); - context = overrideContext; - } - } catch (Resources.NotFoundException e) { - Slog.w(TAG, "failed creating starting window for overrideConfig at taskId: " - + taskId, e); - return null; - } - typedArray.recycle(); + context = overrideContext; } return context; } @@ -367,12 +352,17 @@ public class SplashscreenContentDrawer { /** Extract the window background color from {@code attrs}. */ private static int peekWindowBGColor(Context context, SplashScreenWindowAttrs attrs) { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "peekWindowBGColor"); - final Drawable themeBGDrawable; + Drawable themeBGDrawable = null; if (attrs.mWindowBgColor != 0) { themeBGDrawable = new ColorDrawable(attrs.mWindowBgColor); } else if (attrs.mWindowBgResId != 0) { - themeBGDrawable = context.getDrawable(attrs.mWindowBgResId); - } else { + try { + themeBGDrawable = context.getDrawable(attrs.mWindowBgResId); + } catch (Resources.NotFoundException e) { + Slog.w(TAG, "Unable get drawable from resource", e); + } + } + if (themeBGDrawable == null) { themeBGDrawable = createDefaultBackgroundDrawable(); Slog.w(TAG, "Window background does not exist, using " + themeBGDrawable); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java index e552e6cdacf3..08211ab5df9c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java @@ -53,7 +53,7 @@ import android.window.SplashScreenView; import android.window.StartingWindowInfo; import android.window.StartingWindowRemovalInfo; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.internal.util.ContrastColorUtil; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.protolog.ShellProtoLogGroup; 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 3353c7bd81c2..2e9b53eee13f 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 @@ -33,7 +33,6 @@ import android.hardware.display.DisplayManager; import android.util.SparseArray; import android.view.IWindow; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.view.WindowManager; import android.view.WindowlessWindowManager; import android.window.SplashScreenView; @@ -43,11 +42,11 @@ import android.window.StartingWindowRemovalInfo; import android.window.TaskSnapshot; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.annotations.ShellSplashscreenThread; /** @@ -204,7 +203,7 @@ public class StartingSurfaceDrawer { @Override protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) { - final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + final SurfaceControl.Builder builder = new SurfaceControl.Builder() .setContainerLayer() .setName("Windowless window") .setHidden(false) 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 fa084c585a59..7cb8e8aa7b49 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 @@ -23,7 +23,7 @@ import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_WINDOWLESS; -import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW; +import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW; import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; @@ -48,7 +48,7 @@ import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; -import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; 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 66b3553bea09..6e084d6e05a4 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 @@ -21,8 +21,6 @@ import static android.graphics.Color.WHITE; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; -import static com.android.window.flags.Flags.windowSessionRelayoutInfo; - import android.annotation.BinderThread; import android.annotation.NonNull; import android.annotation.Nullable; @@ -30,7 +28,6 @@ import android.app.ActivityManager; import android.app.ActivityManager.TaskDescription; import android.graphics.Paint; import android.graphics.Rect; -import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.os.Trace; @@ -51,7 +48,7 @@ import android.window.SnapshotDrawerUtils; import android.window.StartingWindowInfo; import android.window.TaskSnapshot; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.internal.view.BaseIWindow; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -139,16 +136,10 @@ public class TaskSnapshotWindow { } try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout"); - if (windowSessionRelayoutInfo()) { - final WindowRelayoutResult outRelayoutResult = new WindowRelayoutResult(tmpFrames, - tmpMergedConfiguration, surfaceControl, tmpInsetsState, tmpControls); - session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, 0, 0, - outRelayoutResult); - } else { - session.relayoutLegacy(window, layoutParams, -1, -1, View.VISIBLE, 0, 0, 0, - tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState, - tmpControls, new Bundle()); - } + final WindowRelayoutResult outRelayoutResult = new WindowRelayoutResult(tmpFrames, + tmpMergedConfiguration, surfaceControl, tmpInsetsState, tmpControls); + session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, 0, 0, + outRelayoutResult); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } catch (RemoteException e) { snapshotSurface.clearWindowSynced(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java index 5c814dcc9b16..2a22d4dd0cb5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java @@ -36,7 +36,7 @@ import android.window.StartingWindowInfo; import android.window.TaskSnapshot; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.shared.TransactionPool; class WindowlessSnapshotWindowCreator { private static final int DEFAULT_FADEOUT_DURATION = 233; @@ -73,7 +73,7 @@ class WindowlessSnapshotWindowCreator { final Display display = mDisplayManager.getDisplay(runningTaskInfo.displayId); final StartingSurfaceDrawer.WindowlessStartingWindow wlw = new StartingSurfaceDrawer.WindowlessStartingWindow( - runningTaskInfo.configuration, rootSurface); + mContext.getResources().getConfiguration(), rootSurface); final SurfaceControlViewHost mViewHost = new SurfaceControlViewHost( mContext, display, wlw, "WindowlessSnapshotWindowCreator"); final Rect windowBounds = runningTaskInfo.configuration.windowConfiguration.getBounds(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java index 98a803128587..e1d760058711 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java @@ -16,7 +16,6 @@ package com.android.wm.shell.startingsurface; -import static android.graphics.Color.WHITE; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN; import android.app.ActivityManager; @@ -37,7 +36,7 @@ import android.window.StartingWindowInfo; import android.window.StartingWindowRemovalInfo; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.shared.TransactionPool; class WindowlessSplashWindowCreator extends AbsSplashWindowCreator { @@ -69,14 +68,15 @@ class WindowlessSplashWindowCreator extends AbsSplashWindowCreator { // Can't show splash screen on requested display, so skip showing at all. return; } + final int theme = getSplashScreenTheme(0 /* splashScreenThemeResId */, activityInfo); final Context myContext = SplashscreenContentDrawer.createContext(mContext, windowInfo, - 0 /* theme */, STARTING_WINDOW_TYPE_SPLASH_SCREEN, mDisplayManager); + theme, STARTING_WINDOW_TYPE_SPLASH_SCREEN, mDisplayManager); if (myContext == null) { return; } final StartingSurfaceDrawer.WindowlessStartingWindow wlw = new StartingSurfaceDrawer.WindowlessStartingWindow( - taskInfo.configuration, rootSurface); + mContext.getResources().getConfiguration(), rootSurface); final SurfaceControlViewHost viewHost = new SurfaceControlViewHost( myContext, display, wlw, "WindowlessSplashWindowCreator"); final String title = "Windowless Splash " + taskInfo.taskId; @@ -86,19 +86,11 @@ class WindowlessSplashWindowCreator extends AbsSplashWindowCreator { final Rect windowBounds = taskInfo.configuration.windowConfiguration.getBounds(); lp.width = windowBounds.width(); lp.height = windowBounds.height(); - final ActivityManager.TaskDescription taskDescription; - if (taskInfo.taskDescription != null) { - taskDescription = taskInfo.taskDescription; - } else { - taskDescription = new ActivityManager.TaskDescription(); - taskDescription.setBackgroundColor(WHITE); - } final FrameLayout rootLayout = new FrameLayout( - mSplashscreenContentDrawer.createViewContextWrapper(mContext)); + mSplashscreenContentDrawer.createViewContextWrapper(myContext)); viewHost.setView(rootLayout, lp); - - final int bgColor = taskDescription.getBackgroundColor(); + final int bgColor = mSplashscreenContentDrawer.estimateTaskBackgroundColor(myContext); final SplashScreenView splashScreenView = mSplashscreenContentDrawer .makeSimpleSplashScreenContentView(myContext, windowInfo, bgColor); rootLayout.addView(splashScreenView); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java index 72fc8686f648..2036d9c13f0c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java @@ -35,7 +35,7 @@ import static android.window.StartingWindowInfo.TYPE_PARAMETER_WINDOWLESS; import android.window.StartingWindowInfo; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellCommandHandler.java index 2e6ddc363906..aa9f15c37531 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellCommandHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellCommandHandler.java @@ -18,7 +18,7 @@ package com.android.wm.shell.sysui; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_INIT; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import java.io.PrintWriter; import java.util.Arrays; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java index 5ced1fb41a41..0202b6cf3eab 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java @@ -40,7 +40,7 @@ import android.view.SurfaceControlRegistry; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; import com.android.wm.shell.common.ExternalInterfaceBinder; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java index 2e2f569a52b8..1140c82a7a08 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java @@ -25,8 +25,9 @@ import android.view.SurfaceControl; import androidx.annotation.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.ArrayList; @@ -47,6 +48,7 @@ public class ShellInit { public ShellInit(ShellExecutor mainExecutor) { mMainExecutor = mainExecutor; + ProtoLog.init(ShellProtoLogGroup.values()); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java index a85188a9e04d..82c0aaf3bc8b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java @@ -118,6 +118,13 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, mTaskViewTaskController.startShortcutActivity(shortcut, options, launchBounds); } + /** + * Moves the current task in taskview out of the view and back to fullscreen. + */ + public void moveToFullscreen() { + mTaskViewTaskController.moveToFullscreen(); + } + @Override public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { if (mTaskViewTaskController.isUsingShellTransitions()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java index a126cbe41b00..e74342e1910c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java @@ -17,6 +17,7 @@ package com.android.wm.shell.taskview; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.WindowManager.TRANSIT_CHANGE; import android.annotation.NonNull; @@ -256,6 +257,24 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { mTaskViewTransitions.startInstantTransition(TRANSIT_CHANGE, wct); } + /** + * Moves the current task in TaskView out of the view and back to fullscreen. + */ + public void moveToFullscreen() { + if (mTaskToken == null) return; + mShellExecutor.execute(() -> { + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setWindowingMode(mTaskToken, WINDOWING_MODE_UNDEFINED); + wct.setAlwaysOnTop(mTaskToken, false); + mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, false); + mTaskViewTransitions.moveTaskViewToFullscreen(wct, this); + if (mListener != null) { + // Task is being "removed" from the clients perspective + mListener.onTaskRemovalStarted(mTaskInfo.taskId); + } + }); + } + private void prepareActivityOptions(ActivityOptions options, Rect launchBounds) { final Binder launchCookie = new Binder(); mShellExecutor.execute(() -> { @@ -535,7 +554,8 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { WindowContainerTransaction wct = new WindowContainerTransaction(); if (mCaptionInsets != null) { wct.addInsetsSource(mTaskToken, mCaptionInsetsOwner, 0, - WindowInsets.Type.captionBar(), mCaptionInsets, null /* boundingRects */); + WindowInsets.Type.captionBar(), mCaptionInsets, null /* boundingRects */, + 0 /* flags */); } else { wct.removeInsetsSource(mTaskToken, mCaptionInsetsOwner, 0, WindowInsets.Type.captionBar()); @@ -584,7 +604,6 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { }); } mTaskViewBase.onTaskVanished(taskInfo); - mTaskOrganizer.setInterceptBackPressedOnTaskRoot(taskInfo.token, false); } } @@ -698,6 +717,9 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { mTaskViewBase.setResizeBgColor(startTransaction, backgroundColor); } + // After the embedded task has appeared, set it to non-trimmable. This is important + // to prevent recents from trimming and removing the embedded task. + wct.setTaskTrimmableFromRecents(taskInfo.token, false /* isTrimmableFromRecents */); mTaskViewBase.onTaskAppeared(mTaskInfo, mTaskLeash); if (mListener != null) { final int taskId = mTaskInfo.taskId; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java index e6d1b4593a46..39648f65b4f3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java @@ -217,6 +217,11 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { return null; } + /** Returns true if the given {@code taskInfo} belongs to a task view. */ + public boolean isTaskViewTask(ActivityManager.RunningTaskInfo taskInfo) { + return findTaskView(taskInfo) != null; + } + void startTaskView(@NonNull WindowContainerTransaction wct, @NonNull TaskViewTaskController taskView, @NonNull IBinder launchCookie) { updateVisibilityState(taskView, true /* visible */); @@ -231,6 +236,12 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { startNextTransition(); } + void moveTaskViewToFullscreen(@NonNull WindowContainerTransaction wct, + @NonNull TaskViewTaskController taskView) { + mPending.add(new PendingTransition(TRANSIT_CHANGE, wct, taskView, null /* cookie */)); + startNextTransition(); + } + /** Starts a new transition to make the given {@code taskView} visible. */ public void setTaskViewVisible(TaskViewTaskController taskView, boolean visible) { setTaskViewVisible(taskView, visible, false /* reorder */); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java index b03daaafd70c..35427b93acea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java @@ -94,6 +94,11 @@ public class CounterRotatorHelper { return rotatedBounds; } + /** Returns true if the change is put on a surface in previous rotation. */ + public boolean isRotated(@NonNull TransitionInfo.Change change) { + return mLastRotationDelta != 0 && mRotatorMap.containsKey(change.getParent()); + } + /** * Removes the counter rotation surface in the finish transaction. No need to reparent the * children as the finish transaction should have already taken care of that. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index 8ee1efa90a30..766a6b3f48ac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -39,7 +39,7 @@ import android.window.TransitionRequestInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.activityembedding.ActivityEmbeddingController; import com.android.wm.shell.common.split.SplitScreenUtils; @@ -353,7 +353,7 @@ public class DefaultMixedHandler implements MixedTransitionHandler, return this::setRecentsTransitionDuringKeyguard; } else if (mDesktopTasksController != null // Check on the default display. Recents/gesture nav is only available there - && mDesktopTasksController.getVisibleTaskCount(DEFAULT_DISPLAY) > 0) { + && mDesktopTasksController.visibleTaskCount(DEFAULT_DISPLAY) > 0) { return this::setRecentsTransitionDuringDesktop; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java index c33fb80fdefc..c8921d256d7f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java @@ -28,7 +28,7 @@ import android.os.IBinder; import android.view.SurfaceControl; import android.window.TransitionInfo; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.activityembedding.ActivityEmbeddingController; import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.pip.PipTransitionController; 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 018c9044e2f7..f40e0bac1b4e 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 @@ -18,6 +18,7 @@ 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_FROM_STYLE; 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; @@ -40,6 +41,7 @@ import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECI import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; import static android.view.WindowManager.TRANSIT_RELAUNCH; +import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL; import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL; import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; @@ -53,6 +55,7 @@ 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_CHANGE; 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; @@ -61,6 +64,7 @@ import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITI import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow; import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet; import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionTypeFromInfo; +import static com.android.wm.shell.transition.TransitionAnimationHelper.isCoveredByOpaqueFullscreenChange; import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation; import android.animation.Animator; @@ -88,7 +92,6 @@ 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; @@ -102,14 +105,14 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.policy.TransitionAnimation; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.window.flags.Flags; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.sysui.ShellInit; @@ -130,8 +133,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private final TransitionAnimation mTransitionAnimation; private final DevicePolicyManager mDevicePolicyManager; - 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<>(); @@ -351,6 +352,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { continue; } final boolean isTask = change.getTaskInfo() != null; + final boolean isFreeform = isTask && change.getTaskInfo().isFreeform(); final int mode = change.getMode(); boolean isSeamlessDisplayChange = false; @@ -457,8 +459,18 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final int layer = zSplitLine + numChanges - i; startTransaction.setLayer(change.getLeash(), layer); } + } else if (!isCoveredByOpaqueFullscreenChange(info, change) + && isFreeform + && TransitionUtil.isOpeningMode(type) + && change.getMode() == TRANSIT_TO_BACK) { + // Reparent the minimize-change to the root task so the minimizing Task + // isn't shown in front of other Tasks. + mRootTDAOrganizer.reparentToDisplayArea( + change.getTaskInfo().displayId, + change.getLeash(), + startTransaction); } else if (isOnlyTranslucent && TransitionUtil.isOpeningType(info.getType()) - && TransitionUtil.isClosingType(mode)) { + && TransitionUtil.isClosingType(mode)) { // If there is a closing translucent task in an OPENING transition, we will // actually select a CLOSING animation, so move the closing task into // the animating part of the z-order. @@ -487,15 +499,19 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { backgroundColorForTransition = getTransitionBackgroundColorIfSet(info, change, a, backgroundColorForTransition); - if (!isTask && a.hasExtension()) { - if (!TransitionUtil.isOpeningType(mode)) { - // Can screenshot now (before startTransaction is applied) - edgeExtendWindow(change, a, startTransaction, finishTransaction); + if (!isTask && a.getExtensionEdges() != 0x0) { + if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()) { + finishTransaction.setEdgeExtensionEffect(change.getLeash(), /* edge */ 0); } else { - // Need to screenshot after startTransaction is applied otherwise activity - // may not be visible or ready yet. - postStartTransactionCallbacks - .add(t -> edgeExtendWindow(change, a, t, finishTransaction)); + if (!TransitionUtil.isOpeningType(mode)) { + // Can screenshot now (before startTransaction is applied) + edgeExtendWindow(change, a, startTransaction, finishTransaction); + } else { + // Need to screenshot after startTransaction is applied otherwise + // activity may not be visible or ready yet. + postStartTransactionCallbacks + .add(t -> edgeExtendWindow(change, a, t, finishTransaction)); + } } } @@ -517,7 +533,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { animRelOffset.y = Math.max(animRelOffset.y, change.getEndRelOffset().y); } - if (change.getActivityComponent() != null && !isActivityLevel) { + if (change.getActivityComponent() != null && !isActivityLevel + && !mRotator.isRotated(change)) { // At this point, this is an independent activity change in a non-activity // transition. This means that an activity transition got erroneously combined // with another ongoing transition. This then means that the animation root may @@ -542,7 +559,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { buildSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish, mTransactionPool, mMainExecutor, animRelOffset, cornerRadius, - clipRect); + clipRect, change.getActivityComponent() != null); final TransitionInfo.AnimationOptions options; if (Flags.moveAnimationOptionsToChange()) { @@ -685,7 +702,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { TransitionInfo.Change change, TransitionInfo info, int animHint, ArrayList<Animator> animations, Runnable onAnimFinish) { final int rootIdx = TransitionUtil.rootIndexFor(change, info); - final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext, mSurfaceSession, + final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext, mTransactionPool, startTransaction, change, info.getRoot(rootIdx).getLeash(), animHint); // The rotation animation may consist of 3 animations: fade-out screenshot, fade-in real @@ -739,6 +756,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { options = info.getAnimationOptions(); } final int overrideType = options != null ? options.getType() : ANIM_NONE; + final int userId = options != null ? options.getUserId() : UserHandle.USER_CURRENT; final Rect endBounds = TransitionUtil.isClosingType(changeMode) ? mRotator.getEndBoundsInStartRotation(change) : change.getEndAbsBounds(); @@ -747,12 +765,12 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { a = mTransitionAnimation.loadKeyguardExitAnimation(flags, (changeFlags & FLAG_SHOW_WALLPAPER) != 0); } else if (type == TRANSIT_KEYGUARD_UNOCCLUDE) { - a = mTransitionAnimation.loadKeyguardUnoccludeAnimation(); + a = mTransitionAnimation.loadKeyguardUnoccludeAnimation(userId); } else if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) { if (isOpeningType) { - a = mTransitionAnimation.loadVoiceActivityOpenAnimation(enter); + a = mTransitionAnimation.loadVoiceActivityOpenAnimation(enter, userId); } else { - a = mTransitionAnimation.loadVoiceActivityExitAnimation(enter); + a = mTransitionAnimation.loadVoiceActivityExitAnimation(enter, userId); } } else if (changeMode == TRANSIT_CHANGE) { // In the absence of a specific adapter, we just want to keep everything stationary. @@ -763,9 +781,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } else if (overrideType == ANIM_CUSTOM && (!isTask || options.getOverrideTaskTransition())) { a = mTransitionAnimation.loadAnimationRes(options.getPackageName(), enter - ? options.getEnterResId() : options.getExitResId()); + ? options.getEnterResId() : options.getExitResId(), userId); } else if (overrideType == ANIM_OPEN_CROSS_PROFILE_APPS && enter) { - a = mTransitionAnimation.loadCrossProfileAppEnterAnimation(); + a = mTransitionAnimation.loadCrossProfileAppEnterAnimation(userId); } else if (overrideType == ANIM_CLIP_REVEAL) { a = mTransitionAnimation.createClipRevealAnimationLocked(type, wallpaperTransit, enter, endBounds, endBounds, options.getTransitionBounds()); @@ -807,7 +825,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @NonNull Animation anim, @NonNull SurfaceControl leash, @NonNull Runnable finishCallback, @NonNull TransactionPool pool, @NonNull ShellExecutor mainExecutor, @Nullable Point position, float cornerRadius, - @Nullable Rect clipRect) { + @Nullable Rect clipRect, boolean isActivity) { final SurfaceControl.Transaction transaction = pool.acquire(); final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); final Transformation transformation = new Transformation(); @@ -819,13 +837,13 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime()); applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix, - position, cornerRadius, clipRect); + position, cornerRadius, clipRect, isActivity); }; va.addUpdateListener(updateListener); final Runnable finisher = () -> { applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix, - position, cornerRadius, clipRect); + position, cornerRadius, clipRect, isActivity); pool.release(transaction); mainExecutor.execute(() -> { @@ -885,9 +903,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final Rect bounds = change.getEndAbsBounds(); // Show the right drawable depending on the user we're transitioning to. final Drawable thumbnailDrawable = change.hasFlags(FLAG_CROSS_PROFILE_OWNER_THUMBNAIL) - ? mContext.getDrawable(R.drawable.ic_account_circle) - : change.hasFlags(FLAG_CROSS_PROFILE_WORK_THUMBNAIL) - ? mEnterpriseThumbnailDrawable : null; + ? mContext.getDrawable(R.drawable.ic_account_circle) + : change.hasFlags(FLAG_CROSS_PROFILE_WORK_THUMBNAIL) + ? mEnterpriseThumbnailDrawable : null; if (thumbnailDrawable == null) { return; } @@ -898,7 +916,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); - final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession, + final WindowThumbnail wt = WindowThumbnail.createAndAttach( change.getLeash(), thumbnail, transaction); final Animation a = mTransitionAnimation.createCrossProfileAppsThumbnailAnimationLocked(bounds); @@ -915,14 +933,15 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { a.restrictDuration(MAX_ANIMATION_DURATION); a.scaleCurrentDuration(mTransitionAnimationScaleSetting); buildSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool, - mMainExecutor, change.getEndRelOffset(), cornerRadius, change.getEndAbsBounds()); + mMainExecutor, change.getEndRelOffset(), cornerRadius, change.getEndAbsBounds(), + change.getActivityComponent() != null); } private void attachThumbnailAnimation(@NonNull ArrayList<Animator> animations, @NonNull Runnable finishCallback, TransitionInfo.Change change, TransitionInfo.AnimationOptions options, float cornerRadius) { final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); - final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession, + final WindowThumbnail wt = WindowThumbnail.createAndAttach( change.getLeash(), options.getThumbnail(), transaction); final Rect bounds = change.getEndAbsBounds(); final int orientation = mContext.getResources().getConfiguration().orientation; @@ -939,16 +958,20 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { a.restrictDuration(MAX_ANIMATION_DURATION); a.scaleCurrentDuration(mTransitionAnimationScaleSetting); buildSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool, - mMainExecutor, change.getEndRelOffset(), cornerRadius, change.getEndAbsBounds()); + mMainExecutor, change.getEndRelOffset(), cornerRadius, change.getEndAbsBounds(), + change.getActivityComponent() != null); } private static int getWallpaperTransitType(TransitionInfo info) { + boolean hasWallpaper = false; 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 ((change.getFlags() & FLAG_SHOW_WALLPAPER) != 0 + || (change.getFlags() & FLAG_IS_WALLPAPER) != 0) { + hasWallpaper = true; if (TransitionUtil.isOpeningType(change.getMode())) { hasOpenWallpaper = true; } else if (TransitionUtil.isClosingType(change.getMode())) { @@ -964,6 +987,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { return WALLPAPER_TRANSITION_OPEN; } else if (hasCloseWallpaper) { return WALLPAPER_TRANSITION_CLOSE; + } else if (hasWallpaper) { + return WALLPAPER_TRANSITION_CHANGE; } else { return WALLPAPER_TRANSITION_NONE; } @@ -978,14 +1003,20 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final int animType = options.getType(); return animType == ANIM_CUSTOM || animType == ANIM_SCALE_UP || animType == ANIM_THUMBNAIL_SCALE_UP || animType == ANIM_THUMBNAIL_SCALE_DOWN - || animType == ANIM_CLIP_REVEAL || animType == ANIM_OPEN_CROSS_PROFILE_APPS; + || animType == ANIM_CLIP_REVEAL || animType == ANIM_OPEN_CROSS_PROFILE_APPS + || animType == ANIM_FROM_STYLE; } private static void applyTransformation(long time, SurfaceControl.Transaction t, SurfaceControl leash, Animation anim, Transformation tmpTransformation, float[] matrix, - Point position, float cornerRadius, @Nullable Rect immutableClipRect) { + Point position, float cornerRadius, @Nullable Rect immutableClipRect, + boolean isActivity) { tmpTransformation.clear(); anim.getTransformation(time, tmpTransformation); + if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader() + && anim.getExtensionEdges() != 0x0 && isActivity) { + t.setEdgeExtensionEffect(leash, anim.getExtensionEdges()); + } if (position != null) { tmpTransformation.getMatrix().postTranslate(position.x, position.y); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java index 9b27e413b5e4..c385f9afcf3a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java @@ -30,6 +30,7 @@ import android.os.IBinder; import android.view.SurfaceControl; import android.window.TransitionInfo; +import com.android.window.flags.Flags; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; @@ -71,9 +72,21 @@ public class HomeTransitionObserver implements TransitionObserver, final int mode = change.getMode(); final boolean isBackGesture = change.hasFlags(FLAG_BACK_GESTURE_ANIMATED); - if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME - && (TransitionUtil.isOpenOrCloseMode(mode) || isBackGesture)) { - notifyHomeVisibilityChanged(TransitionUtil.isOpeningType(mode) || isBackGesture); + if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) { + if (Flags.migratePredictiveBackTransition()) { + final boolean gestureToHomeTransition = isBackGesture + && TransitionUtil.isClosingType(info.getType()); + if (gestureToHomeTransition + || (!isBackGesture && TransitionUtil.isOpenOrCloseMode(mode))) { + notifyHomeVisibilityChanged(gestureToHomeTransition + || TransitionUtil.isOpeningType(mode)); + } + } else { + if (TransitionUtil.isOpenOrCloseMode(mode) || isBackGesture) { + notifyHomeVisibilityChanged(TransitionUtil.isOpeningType(mode) + || isBackGesture); + } + } } } } 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 index 89b0e25b306b..978b8da2eb6d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java @@ -28,7 +28,7 @@ import android.view.SurfaceControl; import android.view.WindowManager; import android.window.IWindowContainerTransactionCallback; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; /** * Utilities and interfaces for transition-like usage on top of the legacy app-transition and diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java index e8b01b5880fb..e61929fef312 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java @@ -20,8 +20,8 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; -import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.shared.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; import static com.android.wm.shell.shared.TransitionUtil.isOpeningMode; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; @@ -35,7 +35,7 @@ import android.annotation.Nullable; import android.view.SurfaceControl; import android.window.TransitionInfo; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -141,10 +141,13 @@ public class MixedTransitionHelper { pipHandler.setEnterAnimationType(ANIM_TYPE_ALPHA); pipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction, finishCB); + // make a new finishTransaction because pip's startEnterAnimation "consumes" it so + // we need a separate one to send over to launcher. + SurfaceControl.Transaction otherFinishT = new SurfaceControl.Transaction(); // Dispatch the rest of the transition normally. This will most-likely be taken by // recents or default handler. mixed.mLeftoversHandler = player.dispatchTransition(mixed.mTransition, everythingElse, - otherStartT, finishTransaction, finishCB, mixedHandler); + otherStartT, otherFinishT, finishCB, mixedHandler); } else { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Not leaving split, so just " + "forward animation to Pip-Handler."); @@ -184,7 +187,8 @@ public class MixedTransitionHelper { for (int i = info.getChanges().size() - 1; i >= 0; --i) { TransitionInfo.Change change = info.getChanges().get(i); - if (change == pipChange || !isOpeningMode(change.getMode())) { + if (change == pipChange || !isOpeningMode(change.getMode()) || + change.getTaskInfo() == null) { // Ignore the change/task that's going into Pip or not opening continue; } 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 e49b03d9bf72..209fc39b096a 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 @@ -30,7 +30,7 @@ import android.window.TransitionRequestInfo; import android.window.WindowAnimationState; import android.window.WindowContainerTransaction; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -192,6 +192,8 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, @Nullable SurfaceControl.Transaction finishTransaction) { try { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "OneShot onTransitionConsumed for %s", mRemote); mRemote.getRemoteTransition().onTransitionConsumed(transition, aborted); } catch (RemoteException e) { Log.e(Transitions.TAG, "Error calling onTransitionConsumed()", e); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java index 9fc6702562bb..fd4d568326d0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java @@ -18,7 +18,7 @@ package com.android.wm.shell.transition; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.transition.DefaultMixedHandler.handoverTransitionLeashes; import static com.android.wm.shell.transition.MixedTransitionHelper.animateEnterPipFromSplit; import static com.android.wm.shell.transition.MixedTransitionHelper.animateKeyguard; @@ -30,7 +30,7 @@ import android.view.SurfaceControl; import android.window.TransitionInfo; import android.window.WindowContainerTransaction; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.pip.PipTransitionController; 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 d6860464d055..dec28fefd789 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 @@ -16,7 +16,7 @@ package com.android.wm.shell.transition; -import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary; +import static com.android.systemui.shared.Flags.returnAnimationFrameworkLongLived; import android.annotation.NonNull; import android.annotation.Nullable; @@ -39,7 +39,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.TransitionUtil; @@ -257,7 +257,7 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { @Override public Transitions.TransitionHandler getHandlerForTakeover( @NonNull IBinder transition, @NonNull TransitionInfo info) { - if (!returnAnimationFrameworkLibrary()) { + if (!returnAnimationFrameworkLongLived()) { return null; } 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 index e196254628d0..5802e2ca8133 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java @@ -38,7 +38,6 @@ 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.ScreenCapture; @@ -47,7 +46,7 @@ import android.window.TransitionInfo; import com.android.internal.R; import com.android.internal.policy.TransitionAnimation; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.shared.TransactionPool; import java.util.ArrayList; @@ -112,7 +111,7 @@ class ScreenRotationAnimation { /** Intensity of light/whiteness of the layout after rotation occurs. */ private float mEndLuma; - ScreenRotationAnimation(Context context, SurfaceSession session, TransactionPool pool, + ScreenRotationAnimation(Context context, TransactionPool pool, Transaction t, TransitionInfo.Change change, SurfaceControl rootLeash, int animHint) { mContext = context; mTransactionPool = pool; @@ -126,7 +125,7 @@ class ScreenRotationAnimation { mStartRotation = change.getStartRotation(); mEndRotation = change.getEndRotation(); - mAnimLeash = new SurfaceControl.Builder(session) + mAnimLeash = new SurfaceControl.Builder() .setParent(rootLeash) .setEffectLayer() .setCallsite("ShellRotationAnimation") @@ -153,7 +152,7 @@ class ScreenRotationAnimation { return; } - mScreenshotLayer = new SurfaceControl.Builder(session) + mScreenshotLayer = new SurfaceControl.Builder() .setParent(mAnimLeash) .setBLASTLayer() .setSecure(screenshotBuffer.containsSecureLayers()) @@ -178,7 +177,7 @@ class ScreenRotationAnimation { t.setCrop(mSurfaceControl, new Rect(0, 0, mEndWidth, mEndHeight)); if (!isCustomRotate()) { - mBackColorSurface = new SurfaceControl.Builder(session) + mBackColorSurface = new SurfaceControl.Builder() .setParent(rootLeash) .setColorLayer() .setOpaque(true) @@ -325,21 +324,21 @@ class ScreenRotationAnimation { @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { buildSurfaceAnimation(animations, mRotateEnterAnimation, mSurfaceControl, finishCallback, mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */, - null /* clipRect */); + null /* clipRect */, false /* isActivity */); } private void startScreenshotRotationAnimation(@NonNull ArrayList<Animator> animations, @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { buildSurfaceAnimation(animations, mRotateExitAnimation, mAnimLeash, finishCallback, mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */, - null /* clipRect */); + null /* clipRect */, false /* isActivity */); } private void buildScreenshotAlphaAnimation(@NonNull ArrayList<Animator> animations, @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { buildSurfaceAnimation(animations, mRotateAlphaAnimation, mAnimLeash, finishCallback, mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */, - null /* clipRect */); + null /* clipRect */, false /* isActivity */); } private void startColorAnimation(float animationScale, @NonNull ShellExecutor animExecutor) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java index 2047b5a88604..a27c14bda15a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java @@ -19,7 +19,9 @@ package com.android.wm.shell.transition; import static android.app.ActivityOptions.ANIM_FROM_STYLE; import static android.app.ActivityOptions.ANIM_NONE; import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION; import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.transitTypeToString; @@ -36,6 +38,7 @@ import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITI import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.WindowConfiguration; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; @@ -54,7 +57,7 @@ import android.window.TransitionInfo; import com.android.internal.R; import com.android.internal.policy.TransitionAnimation; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.window.flags.Flags; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.TransitionUtil; @@ -72,6 +75,9 @@ public class TransitionAnimationHelper { final int changeFlags = change.getFlags(); final boolean enter = TransitionUtil.isOpeningType(changeMode); final boolean isTask = change.getTaskInfo() != null; + final boolean isFreeform = isTask && change.getTaskInfo().isFreeform(); + final boolean isCoveredByOpaqueFullscreenChange = + isCoveredByOpaqueFullscreenChange(info, change); final TransitionInfo.AnimationOptions options; if (Flags.moveAnimationOptionsToChange()) { options = change.getAnimationOptions(); @@ -107,6 +113,24 @@ public class TransitionAnimationHelper { animAttr = enter ? R.styleable.WindowAnimation_wallpaperCloseEnterAnimation : R.styleable.WindowAnimation_wallpaperCloseExitAnimation; + } else if (!isCoveredByOpaqueFullscreenChange + && isFreeform + && TransitionUtil.isOpeningMode(type) + && change.getMode() == TRANSIT_TO_BACK) { + // Set translucent here so TransitionAnimation loads the appropriate animations for + // translucent activities and tasks later + translucent = (changeFlags & FLAG_TRANSLUCENT) != 0; + // The main Task is launching or being brought to front, this Task is being minimized + animAttr = R.styleable.WindowAnimation_activityCloseExitAnimation; + } else if (!isCoveredByOpaqueFullscreenChange + && isFreeform + && type == TRANSIT_TO_FRONT + && change.getMode() == TRANSIT_TO_FRONT) { + // Set translucent here so TransitionAnimation loads the appropriate animations for + // translucent activities and tasks later + translucent = (changeFlags & FLAG_TRANSLUCENT) != 0; + // Bring the minimized Task back to front + animAttr = R.styleable.WindowAnimation_activityOpenEnterAnimation; } else if (type == TRANSIT_OPEN) { // We will translucent open animation for translucent activities and tasks. Choose // WindowAnimation_activityOpenEnterAnimation and set translucent here, then @@ -199,6 +223,15 @@ public class TransitionAnimationHelper { */ public static int getTransitionTypeFromInfo(@NonNull TransitionInfo info) { final int type = info.getType(); + // This back navigation is canceled, check whether the transition should be open or close + if (type == TRANSIT_PREPARE_BACK_NAVIGATION + || type == TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION) { + if (!info.getChanges().isEmpty()) { + final TransitionInfo.Change change = info.getChanges().get(0); + return TransitionUtil.isOpeningMode(change.getMode()) + ? TRANSIT_OPEN : TRANSIT_CLOSE; + } + } // If the info transition type is opening transition, iterate its changes to see if it // has any opening change, if none, returns TRANSIT_CLOSE type for closing animation. if (type == TRANSIT_OPEN) { @@ -417,4 +450,25 @@ public class TransitionAnimationHelper { return edgeExtensionLayer; } + + /** + * Returns whether there is an opaque fullscreen Change positioned in front of the given Change + * in the given TransitionInfo. + */ + static boolean isCoveredByOpaqueFullscreenChange( + TransitionInfo info, TransitionInfo.Change change) { + // TransitionInfo#getChanges() are ordered from front to back + for (TransitionInfo.Change coveringChange : info.getChanges()) { + if (coveringChange == change) { + return false; + } + if ((coveringChange.getFlags() & FLAG_TRANSLUCENT) == 0 + && coveringChange.getTaskInfo() != null + && coveringChange.getTaskInfo().getWindowingMode() + == WindowConfiguration.WINDOWING_MODE_FULLSCREEN) { + return true; + } + } + return false; + } } 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 c3a70bb7d0b7..d03832d3e85e 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 @@ -28,6 +28,7 @@ import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.fixScale; +import static android.view.WindowManager.transitTypeToString; import static android.window.TransitionInfo.FLAGS_IS_NON_APP_WINDOW; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; @@ -37,11 +38,12 @@ import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_NO_ANIMATION; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; +import static com.android.systemui.shared.Flags.returnAnimationFrameworkLongLived; import static com.android.window.flags.Flags.ensureWallpaperInTransitions; -import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary; +import static com.android.window.flags.Flags.migratePredictiveBackTransition; +import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS; import static com.android.wm.shell.shared.TransitionUtil.isClosingType; import static com.android.wm.shell.shared.TransitionUtil.isOpeningType; -import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS; import android.annotation.NonNull; import android.annotation.Nullable; @@ -76,20 +78,19 @@ import androidx.annotation.BinderThread; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; -import com.android.window.flags.Flags; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.IHomeTransitionListener; import com.android.wm.shell.shared.IShellTransitions; import com.android.wm.shell.shared.ShellTransitions; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.shared.annotations.ExternalThread; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -192,6 +193,12 @@ public class Transitions implements RemoteCallable<Transitions>, /** Remote Transition that split accepts but ultimately needs to be animated by the remote. */ public static final int TRANSIT_SPLIT_PASSTHROUGH = TRANSIT_FIRST_CUSTOM + 18; + /** Transition to set windowing mode after exit pip transition is finished animating. */ + public static final int TRANSIT_CLEANUP_PIP_EXIT = WindowManager.TRANSIT_FIRST_CUSTOM + 19; + + /** Transition type to minimize a task. */ + public static final int TRANSIT_MINIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 20; + /** Transition type for desktop mode transitions. */ public static final int TRANSIT_DESKTOP_MODE_TYPES = WindowManager.TRANSIT_FIRST_CUSTOM + 100; @@ -583,14 +590,6 @@ public class Transitions implements RemoteCallable<Transitions>, final boolean isOpening = isOpeningType(transitType); final boolean isClosing = isClosingType(transitType); final int mode = change.getMode(); - // Ensure wallpapers stay in the back - if (change.hasFlags(FLAG_IS_WALLPAPER) && Flags.ensureWallpaperInTransitions()) { - if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { - return -zSplitLine + numChanges - i; - } else { - return -zSplitLine - i; - } - } // Put all the OPEN/SHOW on top if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { if (isOpening) { @@ -833,7 +832,8 @@ public class Transitions implements RemoteCallable<Transitions>, } // The change has already animated by back gesture, don't need to play transition // animation on it. - if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) { + if (!migratePredictiveBackTransition() + && change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) { info.getChanges().remove(i); } } @@ -930,9 +930,12 @@ public class Transitions implements RemoteCallable<Transitions>, } // An existing animation is playing, so see if we can merge. final ActiveTransition playing = track.mActiveTransition; + final IBinder playingToken = playing.mToken; + final IBinder readyToken = ready.mToken; + if (ready.mAborted) { // record as merged since it is no-op. Calls back into processReadyQueue - onMerged(playing, ready); + onMerged(playingToken, readyToken); return; } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s ready while" @@ -940,14 +943,29 @@ public class Transitions implements RemoteCallable<Transitions>, + " in case they can be merged", ready, playing); mTransitionTracer.logMergeRequested(ready.mInfo.getDebugId(), playing.mInfo.getDebugId()); playing.mHandler.mergeAnimation(ready.mToken, ready.mInfo, ready.mStartT, - playing.mToken, (wct) -> onMerged(playing, ready)); + playing.mToken, (wct) -> onMerged(playingToken, readyToken)); } - private void onMerged(@NonNull ActiveTransition playing, @NonNull ActiveTransition merged) { + private void onMerged(@NonNull IBinder playingToken, @NonNull IBinder mergedToken) { + mMainExecutor.assertCurrentThread(); + + ActiveTransition playing = mKnownTransitions.get(playingToken); + if (playing == null) { + Log.e(TAG, "Merging into a non-existent transition: " + playingToken); + return; + } + + ActiveTransition merged = mKnownTransitions.get(mergedToken); + if (merged == null) { + Log.e(TAG, "Merging a non-existent transition: " + mergedToken); + return; + } + if (playing.getTrack() != merged.getTrack()) { throw new IllegalStateException("Can't merge across tracks: " + merged + " into " + playing); } + final Track track = mTracks.get(playing.getTrack()); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged: %s into %s", merged, playing); @@ -1077,13 +1095,15 @@ public class Transitions implements RemoteCallable<Transitions>, info.releaseAnimSurfaces(); } - private void onFinish(IBinder token, - @Nullable WindowContainerTransaction wct) { + private void onFinish(IBinder token, @Nullable WindowContainerTransaction wct) { + mMainExecutor.assertCurrentThread(); + final ActiveTransition active = mKnownTransitions.get(token); if (active == null) { Log.e(TAG, "Trying to finish a non-existent transition: " + token); return; } + final Track track = mTracks.get(active.getTrack()); if (track == null || track.mActiveTransition != active) { Log.e(TAG, "Trying to finish a non-running transition. Either remote crashed or " @@ -1173,12 +1193,15 @@ public class Transitions implements RemoteCallable<Transitions>, } if (request.getDisplayChange() != null) { TransitionRequestInfo.DisplayChange change = request.getDisplayChange(); - if (change.getEndRotation() != change.getStartRotation()) { - // Is a rotation, so dispatch to all displayChange listeners + if (change.getStartRotation() != change.getEndRotation() + || (change.getStartAbsBounds() != null + && !change.getStartAbsBounds().equals(change.getEndAbsBounds()))) { + // Is a display change, so dispatch to all displayChange listeners if (wct == null) { wct = new WindowContainerTransaction(); } - mDisplayController.onDisplayRotateRequested(wct, change.getDisplayId(), + mDisplayController.onDisplayChangeRequested(wct, change.getDisplayId(), + change.getStartAbsBounds(), change.getEndAbsBounds(), change.getStartRotation(), change.getEndRotation()); } } @@ -1213,7 +1236,7 @@ public class Transitions implements RemoteCallable<Transitions>, public IBinder startTransition(@WindowManager.TransitionType int type, @NonNull WindowContainerTransaction wct, @Nullable TransitionHandler handler) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Directly starting a new transition " - + "type=%d wct=%s handler=%s", type, wct, handler); + + "type=%s wct=%s handler=%s", transitTypeToString(type), wct, handler); final ActiveTransition active = new ActiveTransition(mOrganizer.startNewTransition(type, wct)); active.mHandler = handler; @@ -1229,7 +1252,7 @@ public class Transitions implements RemoteCallable<Transitions>, @Nullable public TransitionHandler getHandlerForTakeover( @NonNull IBinder transition, @NonNull TransitionInfo info) { - if (!returnAnimationFrameworkLibrary()) { + if (!returnAnimationFrameworkLongLived()) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "Trying to get a handler for takeover but the flag is disabled"); return null; @@ -1478,16 +1501,16 @@ public class Transitions implements RemoteCallable<Transitions>, * transition animation. The Transition system will apply it when * finishCallback is called by the transition handler. */ - void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info, + default void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction); + @NonNull SurfaceControl.Transaction finishTransaction) {} /** * Called when the transition is starting to play. It isn't called for merged transitions. * * @param transition the unique token of this transition */ - void onTransitionStarting(@NonNull IBinder transition); + default void onTransitionStarting(@NonNull IBinder transition) {} /** * Called when a transition is merged into another transition. There won't be any following @@ -1496,7 +1519,7 @@ public class Transitions implements RemoteCallable<Transitions>, * @param merged the unique token of the transition that's merged to another one * @param playing the unique token of the transition that accepts the merge */ - void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing); + default void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {} /** * Called when the transition is finished. This isn't called for merged transitions. @@ -1504,7 +1527,7 @@ public class Transitions implements RemoteCallable<Transitions>, * @param transition the unique token of this transition * @param aborted {@code true} if this transition is aborted; {@code false} otherwise. */ - void onTransitionFinished(@NonNull IBinder transition, boolean aborted); + default void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {} } @BinderThread 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 index 2c668ed3d84d..341f2bc66716 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java @@ -21,7 +21,6 @@ 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. @@ -33,10 +32,10 @@ class WindowThumbnail { private WindowThumbnail() {} /** Create a thumbnail surface and attach it over a parent surface. */ - static WindowThumbnail createAndAttach(SurfaceSession surfaceSession, SurfaceControl parent, + static WindowThumbnail createAndAttach(SurfaceControl parent, HardwareBuffer thumbnailHeader, SurfaceControl.Transaction t) { WindowThumbnail windowThumbnail = new WindowThumbnail(); - windowThumbnail.mSurfaceControl = new SurfaceControl.Builder(surfaceSession) + windowThumbnail.mSurfaceControl = new SurfaceControl.Builder() .setParent(parent) .setName("WindowThumanil : " + parent.toString()) .setCallsite("WindowThumanil") diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java index e6d35e83116b..2ca749c276e7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java @@ -23,7 +23,7 @@ import android.util.SparseArray; import android.view.SurfaceControl; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener; import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java index 7c2ba455c0c9..f783b45589b3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java @@ -33,8 +33,8 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.internal.protolog.common.ProtoLog; -import com.android.wm.shell.common.TransactionPool; +import com.android.internal.protolog.ProtoLog; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java index bb5d54652460..d28287da83b6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java @@ -19,8 +19,8 @@ package com.android.wm.shell.unfold.animation; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.view.Display.DEFAULT_DISPLAY; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; @@ -41,7 +41,7 @@ import android.view.WindowInsets; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.wm.shell.common.DisplayInsetsController; -import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; +import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener; import com.android.wm.shell.splitscreen.SplitScreenController; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt deleted file mode 100644 index 564e716c7378..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.util - -import android.util.Log -import com.android.internal.protolog.common.IProtoLogGroup -import com.android.internal.protolog.common.ProtoLog - -/** - * Log messages using an API similar to [com.android.internal.protolog.common.ProtoLog]. Useful for - * logging from Kotlin classes as ProtoLog does not have support for Kotlin. - * - * All messages are logged to logcat if logging is enabled for that [IProtoLogGroup]. - */ -// TODO(b/168581922): remove once ProtoLog adds support for Kotlin -class KtProtoLog { - companion object { - /** @see [com.android.internal.protolog.common.ProtoLog.d] */ - fun d(group: IProtoLogGroup, messageString: String, vararg args: Any) { - if (group.isLogToLogcat) { - Log.d(group.tag, String.format(messageString, *args)) - } - } - - /** @see [com.android.internal.protolog.common.ProtoLog.v] */ - fun v(group: IProtoLogGroup, messageString: String, vararg args: Any) { - if (group.isLogToLogcat) { - Log.v(group.tag, String.format(messageString, *args)) - } - } - - /** @see [com.android.internal.protolog.common.ProtoLog.i] */ - fun i(group: IProtoLogGroup, messageString: String, vararg args: Any) { - if (group.isLogToLogcat) { - Log.i(group.tag, String.format(messageString, *args)) - } - } - - /** @see [com.android.internal.protolog.common.ProtoLog.w] */ - fun w(group: IProtoLogGroup, messageString: String, vararg args: Any) { - if (group.isLogToLogcat) { - Log.w(group.tag, String.format(messageString, *args)) - } - } - - /** @see [com.android.internal.protolog.common.ProtoLog.e] */ - fun e(group: IProtoLogGroup, messageString: String, vararg args: Any) { - if (group.isLogToLogcat) { - Log.e(group.tag, String.format(messageString, *args)) - } - } - - /** @see [com.android.internal.protolog.common.ProtoLog.wtf] */ - fun wtf(group: IProtoLogGroup, messageString: String, vararg args: Any) { - if (group.isLogToLogcat) { - Log.wtf(group.tag, String.format(messageString, *args)) - } - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/util/OWNERS deleted file mode 100644 index 482aaab6bc74..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/OWNERS +++ /dev/null @@ -1 +0,0 @@ -per-file KtProtolog.kt = file:platform/development:/tools/winscope/OWNERS diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/AbstractTaskPositionerDecorator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/AbstractTaskPositionerDecorator.kt new file mode 100644 index 000000000000..6dd5ac60b063 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/AbstractTaskPositionerDecorator.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 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.windowdecor + +/** + * Abstract decorator for a [TaskPositioner]. + */ +abstract class AbstractTaskPositionerDecorator( + private val taskPositioner: TaskPositioner +) : TaskPositioner by taskPositioner diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 95e0d79c212e..431461a27e6f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -19,6 +19,7 @@ package com.android.wm.shell.windowdecor; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.content.pm.PackageManager.FEATURE_PC; import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS; import static android.view.WindowManager.TRANSIT_CHANGE; @@ -26,15 +27,24 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import android.app.ActivityManager.RunningTaskInfo; import android.content.ContentResolver; import android.content.Context; +import android.graphics.Point; import android.graphics.Rect; +import android.graphics.Region; +import android.hardware.input.InputManager; import android.os.Handler; +import android.os.RemoteException; +import android.os.UserHandle; import android.provider.Settings; +import android.util.Log; import android.util.SparseArray; import android.view.Choreographer; import android.view.Display; +import android.view.ISystemGestureExclusionListener; +import android.view.IWindowManager; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; +import android.view.ViewConfiguration; import android.window.DisplayAreaInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -45,48 +55,103 @@ import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.windowdecor.extension.TaskInfoKt; +import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier; /** * View model for the window decoration with a caption and shadows. Works with * {@link CaptionWindowDecoration}. */ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { + private static final String TAG = "CaptionWindowDecorViewModel"; + private final ShellTaskOrganizer mTaskOrganizer; + private final IWindowManager mWindowManager; private final Context mContext; private final Handler mMainHandler; + private final @ShellBackgroundThread ShellExecutor mBgExecutor; + private final ShellExecutor mMainExecutor; private final Choreographer mMainChoreographer; private final DisplayController mDisplayController; private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private final SyncTransactionQueue mSyncQueue; private final Transitions mTransitions; + private final Region mExclusionRegion = Region.obtain(); + private final InputManager mInputManager; + private final WindowDecorViewHostSupplier mWindowDecorViewHostSupplier; private TaskOperations mTaskOperations; + /** + * Whether to pilfer the next motion event to send cancellations to the windows below. + * Useful when the caption window is spy and the gesture should be handled by the system + * instead of by the app for their custom header content. + */ + private boolean mShouldPilferCaptionEvents; + private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>(); + private final ISystemGestureExclusionListener mGestureExclusionListener = + new ISystemGestureExclusionListener.Stub() { + @Override + public void onSystemGestureExclusionChanged(int displayId, + Region systemGestureExclusion, Region systemGestureExclusionUnrestricted) { + if (mContext.getDisplayId() != displayId) { + return; + } + mMainExecutor.execute(() -> { + mExclusionRegion.set(systemGestureExclusion); + }); + } + }; + public CaptionWindowDecorViewModel( Context context, Handler mainHandler, + @ShellBackgroundThread ShellExecutor bgExecutor, + ShellExecutor shellExecutor, Choreographer mainChoreographer, + IWindowManager windowManager, + ShellInit shellInit, ShellTaskOrganizer taskOrganizer, DisplayController displayController, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, SyncTransactionQueue syncQueue, - Transitions transitions) { + Transitions transitions, + WindowDecorViewHostSupplier windowDecorViewHostSupplier) { mContext = context; + mMainExecutor = shellExecutor; mMainHandler = mainHandler; + mBgExecutor = bgExecutor; + mWindowManager = windowManager; mMainChoreographer = mainChoreographer; mTaskOrganizer = taskOrganizer; mDisplayController = displayController; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mSyncQueue = syncQueue; mTransitions = transitions; + mWindowDecorViewHostSupplier = windowDecorViewHostSupplier; if (!Transitions.ENABLE_SHELL_TRANSITIONS) { mTaskOperations = new TaskOperations(null, mContext, mSyncQueue); } + mInputManager = mContext.getSystemService(InputManager.class); + + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + try { + mWindowManager.registerSystemGestureExclusionListener(mGestureExclusionListener, + mContext.getDisplayId()); + } catch (RemoteException e) { + Log.e(TAG, "Failed to register window manager callbacks", e); + } } @Override @@ -114,8 +179,12 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { if (decoration == null) return; + if (!shouldShowWindowDecor(taskInfo)) { + destroyWindowDecoration(taskInfo); + return; + } + decoration.relayout(taskInfo); - setupCaptionColor(taskInfo, decoration); } @Override @@ -177,15 +246,13 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { decoration.close(); } - private void setupCaptionColor(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) { - final int statusBarColor = taskInfo.taskDescription.getStatusBarColor(); - decoration.setCaptionColor(statusBarColor); - } - private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { return true; } + if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) { + return false; + } if (taskInfo.getActivityType() != ACTIVITY_TYPE_STANDARD) { return false; } @@ -225,13 +292,16 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { final CaptionWindowDecoration windowDecoration = new CaptionWindowDecoration( mContext, + mContext.createContextAsUser(UserHandle.of(taskInfo.userId), 0 /* flags */), mDisplayController, mTaskOrganizer, taskInfo, taskSurface, mMainHandler, + mBgExecutor, mMainChoreographer, - mSyncQueue); + mSyncQueue, + mWindowDecorViewHostSupplier); mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); final FluidResizeTaskPositioner taskPositioner = @@ -241,11 +311,9 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { new CaptionTouchEventListener(taskInfo, taskPositioner); windowDecoration.setCaptionListeners(touchEventListener, touchEventListener); windowDecoration.setDragPositioningCallback(taskPositioner); - windowDecoration.setDragDetector(touchEventListener.mDragDetector); windowDecoration.setTaskDragResizer(taskPositioner); windowDecoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* setTaskCropAndPosition */); - setupCaptionColor(taskInfo, windowDecoration); } private class CaptionTouchEventListener implements @@ -266,7 +334,8 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mTaskId = taskInfo.taskId; mTaskToken = taskInfo.token; mDragPositioningCallback = dragPositioningCallback; - mDragDetector = new DragDetector(this); + mDragDetector = new DragDetector(this, 0 /* holdToDragMinDurationMs */, + ViewConfiguration.get(mContext).getScaledTouchSlop()); mDisplayId = taskInfo.displayId; } @@ -301,6 +370,49 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mSyncQueue.queue(wct); } } + final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); + + final int actionMasked = e.getActionMasked(); + final boolean isDown = actionMasked == MotionEvent.ACTION_DOWN; + final boolean isUpOrCancel = actionMasked == MotionEvent.ACTION_CANCEL + || actionMasked == MotionEvent.ACTION_UP; + if (isDown) { + final boolean downInCustomizableCaptionRegion = + decoration.checkTouchEventInCustomizableRegion(e); + final boolean downInExclusionRegion = mExclusionRegion.contains( + (int) e.getRawX(), (int) e.getRawY()); + final boolean isTransparentCaption = + TaskInfoKt.isTransparentCaptionBarAppearance(decoration.mTaskInfo); + // MotionEvent's coordinates are relative to view, we want location in window + // to offset position relative to caption as a whole. + int[] viewLocation = new int[2]; + v.getLocationInWindow(viewLocation); + final boolean isResizeEvent = decoration.shouldResizeListenerHandleEvent(e, + new Point(viewLocation[0], viewLocation[1])); + // The caption window may be a spy window when the caption background is + // transparent, which means events will fall through to the app window. Make + // sure to cancel these events if they do not happen in the intersection of the + // customizable region and what the app reported as exclusion areas, because + // the drag-move or other caption gestures should take priority outside those + // regions. + mShouldPilferCaptionEvents = !(downInCustomizableCaptionRegion + && downInExclusionRegion && isTransparentCaption) && !isResizeEvent; + } + + if (!mShouldPilferCaptionEvents) { + // The event will be handled by a window below or pilfered by resize handler. + return false; + } + // Otherwise pilfer so that windows below receive cancellations for this gesture, and + // continue normal handling as a caption gesture. + if (mInputManager != null) { + // TODO(b/352127475): Only pilfer once per gesture + mInputManager.pilferPointers(v.getViewRootImpl().getInputToken()); + } + if (isUpOrCancel) { + // Gesture is finished, reset state. + mShouldPilferCaptionEvents = false; + } return mDragDetector.onMotionEvent(e); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index d0ca5b0fdce6..d0eba23f7d45 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -16,11 +16,16 @@ package com.android.wm.shell.windowdecor; +import static android.window.flags.DesktopModeFlags.ENABLE_WINDOWING_SCALED_RESIZING; + import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; +import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeHandleEdgeInset; import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; @@ -28,22 +33,32 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Color; +import android.graphics.Insets; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.VectorDrawable; import android.os.Handler; import android.util.Size; import android.view.Choreographer; +import android.view.InsetsState; +import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; import android.view.ViewConfiguration; +import android.view.WindowInsets; +import android.view.WindowManager; import android.window.WindowContainerTransaction; +import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; 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.SyncTransactionQueue; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; +import com.android.wm.shell.windowdecor.extension.TaskInfoKt; +import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier; /** * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with @@ -52,6 +67,7 @@ import com.android.wm.shell.common.SyncTransactionQueue; */ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { private final Handler mHandler; + private final @ShellBackgroundThread ShellExecutor mBgExecutor; private final Choreographer mChoreographer; private final SyncTransactionQueue mSyncQueue; @@ -59,7 +75,6 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL private View.OnTouchListener mOnCaptionTouchListener; private DragPositioningCallback mDragPositioningCallback; private DragResizeInputListener mDragResizeListener; - private DragDetector mDragDetector; private RelayoutParams mRelayoutParams = new RelayoutParams(); private final RelayoutResult<WindowDecorLinearLayout> mResult = @@ -67,15 +82,20 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL CaptionWindowDecoration( Context context, + @NonNull Context userContext, DisplayController displayController, ShellTaskOrganizer taskOrganizer, RunningTaskInfo taskInfo, SurfaceControl taskSurface, Handler handler, + @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, - SyncTransactionQueue syncQueue) { - super(context, displayController, taskOrganizer, taskInfo, taskSurface); + SyncTransactionQueue syncQueue, + WindowDecorViewHostSupplier windowDecorViewHostSupplier) { + super(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface, + windowDecorViewHostSupplier); mHandler = handler; + mBgExecutor = bgExecutor; mChoreographer = choreographer; mSyncQueue = syncQueue; } @@ -156,12 +176,6 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL return stableBounds.bottom - requiredEmptySpace; } - - void setDragDetector(DragDetector dragDetector) { - mDragDetector = dragDetector; - mDragDetector.setTouchSlop(ViewConfiguration.get(mContext).getScaledTouchSlop()); - } - @Override void relayout(RunningTaskInfo taskInfo) { final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); @@ -177,32 +191,64 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL shouldSetTaskPositionAndCrop); } + @VisibleForTesting + static void updateRelayoutParams( + RelayoutParams relayoutParams, + ActivityManager.RunningTaskInfo taskInfo, + boolean applyStartTransactionOnDraw, + boolean setTaskCropAndPosition, + InsetsState displayInsetsState) { + relayoutParams.reset(); + relayoutParams.mRunningTaskInfo = taskInfo; + relayoutParams.mLayoutResId = R.layout.caption_window_decor; + relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode()); + relayoutParams.mShadowRadiusId = taskInfo.isFocused + ? R.dimen.freeform_decor_shadow_focused_thickness + : R.dimen.freeform_decor_shadow_unfocused_thickness; + relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; + relayoutParams.mSetTaskPositionAndCrop = setTaskCropAndPosition; + + if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { + // If the app is requesting to customize the caption bar, allow input to fall + // through to the windows below so that the app can respond to input events on + // their custom content. + relayoutParams.mInputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY; + } + final RelayoutParams.OccludingCaptionElement backButtonElement = + new RelayoutParams.OccludingCaptionElement(); + backButtonElement.mWidthResId = R.dimen.caption_left_buttons_width; + backButtonElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.START; + relayoutParams.mOccludingCaptionElements.add(backButtonElement); + // Then, the right-aligned section (minimize, maximize and close buttons). + final RelayoutParams.OccludingCaptionElement controlsElement = + new RelayoutParams.OccludingCaptionElement(); + controlsElement.mWidthResId = R.dimen.caption_right_buttons_width; + controlsElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.END; + relayoutParams.mOccludingCaptionElements.add(controlsElement); + relayoutParams.mCaptionTopPadding = getTopPadding(relayoutParams, + taskInfo.getConfiguration().windowConfiguration.getBounds(), displayInsetsState); + } + + @SuppressLint("MissingPermission") void relayout(RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition) { - final int shadowRadiusID = taskInfo.isFocused - ? R.dimen.freeform_decor_shadow_focused_thickness - : R.dimen.freeform_decor_shadow_unfocused_thickness; final boolean isFreeform = taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; - final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; + final boolean isDragResizeable = ENABLE_WINDOWING_SCALED_RESIZING.isTrue() + ? isFreeform : isFreeform && taskInfo.isResizeable; final WindowDecorLinearLayout oldRootView = mResult.mRootView; final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; final WindowContainerTransaction wct = new WindowContainerTransaction(); - mRelayoutParams.reset(); - mRelayoutParams.mRunningTaskInfo = taskInfo; - mRelayoutParams.mLayoutResId = R.layout.caption_window_decor; - mRelayoutParams.mCaptionHeightId = getCaptionHeightId(taskInfo.getWindowingMode()); - mRelayoutParams.mShadowRadiusId = shadowRadiusID; - mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; - mRelayoutParams.mSetTaskPositionAndCrop = setTaskCropAndPosition; + updateRelayoutParams(mRelayoutParams, taskInfo, applyStartTransactionOnDraw, + setTaskCropAndPosition, mDisplayController.getInsetsState(taskInfo.displayId)); relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo - mTaskOrganizer.applyTransaction(wct); + mBgExecutor.execute(() -> mTaskOrganizer.applyTransaction(wct)); if (mResult.mRootView == null) { // This means something blocks the window decor from showing, e.g. the task is hidden. @@ -213,6 +259,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL setupRootView(); } + bindData(mResult.mRootView, taskInfo); + if (!isDragResizeable) { closeDragResizeListener(); return; @@ -234,12 +282,12 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()) .getScaledTouchSlop(); - mDragDetector.setTouchSlop(touchSlop); final Resources res = mResult.mRootView.getResources(); mDragResizeListener.setGeometry(new DragResizeWindowGeometry(0 /* taskCornerRadius */, new Size(mResult.mWidth, mResult.mHeight), getResizeEdgeHandleSize(res), - getFineResizeCornerSize(res), getLargeResizeCornerSize(res)), touchSlop); + getResizeHandleEdgeInset(res), getFineResizeCornerSize(res), + getLargeResizeCornerSize(res)), touchSlop); } /** @@ -258,7 +306,27 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL maximize.setOnClickListener(mOnCaptionButtonClickListener); } - void setCaptionColor(int captionColor) { + private void bindData(View rootView, RunningTaskInfo taskInfo) { + // Set up the tint first so that the drawable can be stylized when loaded. + setupCaptionColor(taskInfo); + + final boolean isFullscreen = + taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + rootView.findViewById(R.id.maximize_window) + .setBackgroundResource(isFullscreen ? R.drawable.decor_restore_button_dark + : R.drawable.decor_maximize_button_dark); + } + + private void setupCaptionColor(RunningTaskInfo taskInfo) { + if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { + setCaptionColor(Color.TRANSPARENT); + } else { + final int statusBarColor = taskInfo.taskDescription.getStatusBarColor(); + setCaptionColor(statusBarColor); + } + } + + private void setCaptionColor(int captionColor) { if (mResult.mRootView == null) { return; } @@ -275,20 +343,16 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL caption.getResources().getColorStateList(buttonTintColorRes, null /* theme */); final View back = caption.findViewById(R.id.back_button); - final VectorDrawable backBackground = (VectorDrawable) back.getBackground(); - backBackground.setTintList(buttonTintColor); + back.setBackgroundTintList(buttonTintColor); final View minimize = caption.findViewById(R.id.minimize_window); - final VectorDrawable minimizeBackground = (VectorDrawable) minimize.getBackground(); - minimizeBackground.setTintList(buttonTintColor); + minimize.setBackgroundTintList(buttonTintColor); final View maximize = caption.findViewById(R.id.maximize_window); - final VectorDrawable maximizeBackground = (VectorDrawable) maximize.getBackground(); - maximizeBackground.setTintList(buttonTintColor); + maximize.setBackgroundTintList(buttonTintColor); final View close = caption.findViewById(R.id.close_window); - final VectorDrawable closeBackground = (VectorDrawable) close.getBackground(); - closeBackground.setTintList(buttonTintColor); + close.setBackgroundTintList(buttonTintColor); } boolean isHandlingDragResize() { @@ -303,6 +367,29 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL mDragResizeListener = null; } + private static int getTopPadding(RelayoutParams params, Rect taskBounds, + InsetsState insetsState) { + if (!params.mRunningTaskInfo.isFreeform()) { + Insets systemDecor = insetsState.calculateInsets(taskBounds, + WindowInsets.Type.systemBars() & ~WindowInsets.Type.captionBar(), + false /* ignoreVisibility */); + return systemDecor.top; + } else { + return 0; + } + } + + /** + * Checks whether the touch event falls inside the customizable caption region. + */ + boolean checkTouchEventInCustomizableRegion(MotionEvent ev) { + return mResult.mCustomizableCaptionRegion.contains((int) ev.getRawX(), (int) ev.getRawY()); + } + + boolean shouldResizeListenerHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) { + return mDragResizeListener != null && mDragResizeListener.shouldHandleEvent(e, offset); + } + @Override public void close() { closeDragResizeListener(); @@ -311,6 +398,10 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL @Override int getCaptionHeightId(@WindowingMode int windowingMode) { + return getCaptionHeightIdStatic(windowingMode); + } + + private static int getCaptionHeightIdStatic(@WindowingMode int windowingMode) { return R.dimen.freeform_decor_caption_height; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt new file mode 100644 index 000000000000..13a805aef0f1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2024 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.windowdecor + +import android.app.ActivityManager.RunningTaskInfo +import android.content.Context +import android.graphics.Point +import android.graphics.Rect +import android.view.WindowManager +import android.window.TaskSnapshot +import androidx.compose.ui.graphics.toArgb +import com.android.wm.shell.shared.desktopmode.ManageWindowsViewContainer +import com.android.wm.shell.shared.split.SplitScreenConstants +import com.android.wm.shell.splitscreen.SplitScreenController +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer +import com.android.wm.shell.windowdecor.common.DecorThemeUtil +import com.android.wm.shell.windowdecor.extension.isFullscreen +import com.android.wm.shell.windowdecor.extension.isMultiWindow + +/** + * Implementation of [ManageWindowsViewContainer] meant to be used in desktop header and app + * handle. + */ +class DesktopHandleManageWindowsMenu( + private val callerTaskInfo: RunningTaskInfo, + private val splitScreenController: SplitScreenController, + private val captionX: Int, + private val captionWidth: Int, + private val windowManagerWrapper: WindowManagerWrapper, + context: Context, + snapshotList: List<Pair<Int, TaskSnapshot>>, + onIconClickListener: ((Int) -> Unit), + onOutsideClickListener: (() -> Unit) +) : ManageWindowsViewContainer( + context, + DecorThemeUtil(context).getColorScheme(callerTaskInfo).background.toArgb() +) { + private var menuViewContainer: AdditionalViewContainer? = null + + init { + show(snapshotList, onIconClickListener, onOutsideClickListener) + } + + override fun close() { + menuViewContainer?.releaseView() + } + + private fun calculateMenuPosition(): Point { + val position = Point() + val nonFreeformX = (captionX + (captionWidth / 2) - (menuView.menuWidth / 2)) + when { + callerTaskInfo.isFreeform -> { + val taskBounds = callerTaskInfo.getConfiguration().windowConfiguration.bounds + position.set(taskBounds.left, taskBounds.top) + } + callerTaskInfo.isFullscreen -> { + position.set(nonFreeformX, 0) + } + callerTaskInfo.isMultiWindow -> { + val splitPosition = splitScreenController.getSplitPosition(callerTaskInfo.taskId) + val leftOrTopStageBounds = Rect() + val rightOrBottomStageBounds = Rect() + splitScreenController.getStageBounds(leftOrTopStageBounds, rightOrBottomStageBounds) + // TODO(b/343561161): This needs to be calculated differently if the task is in + // top/bottom split. + when (splitPosition) { + SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT -> { + position.set(leftOrTopStageBounds.width() + nonFreeformX, /* y= */ 0) + } + + SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT -> { + position.set(nonFreeformX, /* y= */ 0) + } + } + } + } + return position + } + + override fun addToContainer(menuView: ManageWindowsView) { + val menuPosition = calculateMenuPosition() + menuViewContainer = AdditionalSystemViewContainer( + windowManagerWrapper, + callerTaskInfo.taskId, + menuPosition.x, + menuPosition.y, + menuView.menuWidth, + menuView.menuHeight, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, + menuView.rootView + ) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt new file mode 100644 index 000000000000..05391a8343a5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 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.windowdecor + +import android.app.ActivityManager.RunningTaskInfo +import android.content.Context +import android.graphics.PixelFormat +import android.graphics.Point +import android.view.SurfaceControl +import android.view.SurfaceControlViewHost +import android.view.WindowManager +import android.view.WindowlessWindowManager +import android.window.TaskConstants +import android.window.TaskSnapshot +import androidx.compose.ui.graphics.toArgb +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.shared.desktopmode.ManageWindowsViewContainer +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer +import com.android.wm.shell.windowdecor.common.DecorThemeUtil +import java.util.function.Supplier + +/** + * Implementation of [ManageWindowsViewContainer] meant to be used in desktop header and app + * handle. + */ +class DesktopHeaderManageWindowsMenu( + private val callerTaskInfo: RunningTaskInfo, + private val displayController: DisplayController, + private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer, + context: Context, + private val surfaceControlBuilderSupplier: Supplier<SurfaceControl.Builder>, + private val surfaceControlTransactionSupplier: Supplier<SurfaceControl.Transaction>, + snapshotList: List<Pair<Int, TaskSnapshot>>, + onIconClickListener: ((Int) -> Unit), + onOutsideClickListener: (() -> Unit) +) : ManageWindowsViewContainer( + context, + DecorThemeUtil(context).getColorScheme(callerTaskInfo).background.toArgb() +) { + private var menuViewContainer: AdditionalViewContainer? = null + + init { + show(snapshotList, onIconClickListener, onOutsideClickListener) + } + + override fun close() { + menuViewContainer?.releaseView() + } + + override fun addToContainer(menuView: ManageWindowsView) { + val taskBounds = callerTaskInfo.getConfiguration().windowConfiguration.bounds + val menuPosition = Point(taskBounds.left, taskBounds.top) + val builder = surfaceControlBuilderSupplier.get() + rootTdaOrganizer.attachToDisplayArea(callerTaskInfo.displayId, builder) + val leash = builder + .setName("Manage Windows Menu") + .setContainerLayer() + .build() + val lp = WindowManager.LayoutParams( + menuView.menuWidth, menuView.menuHeight, + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH + or WindowManager.LayoutParams.FLAG_SPLIT_TOUCH, + PixelFormat.TRANSPARENT + ) + val windowManager = WindowlessWindowManager( + callerTaskInfo.configuration, + leash, + null // HostInputToken + ) + val viewHost = SurfaceControlViewHost( + context, + displayController.getDisplay(callerTaskInfo.displayId), windowManager, + "MaximizeMenu" + ) + menuView.let { viewHost.setView(it.rootView, lp) } + val t = surfaceControlTransactionSupplier.get() + t.setLayer(leash, TaskConstants.TASK_CHILD_LAYER_FLOATING_MENU) + .setPosition(leash, menuPosition.x.toFloat(), menuPosition.y.toFloat()) + .show(leash) + t.apply() + menuViewContainer = AdditionalViewHostViewContainer( + leash, viewHost, + surfaceControlTransactionSupplier + ) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 21b6db29143a..c59d92912e80 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -22,35 +22,48 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; 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.content.Intent.ACTION_MAIN; +import static android.content.Intent.CATEGORY_APP_BROWSER; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.view.InputDevice.SOURCE_TOUCHSCREEN; import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_HOVER_ENTER; import static android.view.MotionEvent.ACTION_HOVER_EXIT; -import static android.view.MotionEvent.ACTION_HOVER_MOVE; import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_UP; import static android.view.WindowInsets.Type.statusBars; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.compatui.AppCompatUtils.isSingleTopActivityTranslucent; +import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU; +import static com.android.wm.shell.compatui.AppCompatUtils.isTopActivityExemptFromDesktopWindowing; import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR; +import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR; +import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; +import static com.android.wm.shell.shared.desktopmode.ManageWindowsViewContainer.MANAGE_WINDOWS_MINIMUM_INSTANCES; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import android.annotation.NonNull; import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityTaskManager; +import android.app.IActivityManager; +import android.app.IActivityTaskManager; import android.content.Context; +import android.content.Intent; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; import android.hardware.input.InputManager; +import android.net.Uri; import android.os.Handler; +import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; +import android.os.UserHandle; import android.util.Log; import android.util.SparseArray; import android.view.Choreographer; @@ -61,36 +74,50 @@ import android.view.InputChannel; import android.view.InputEvent; import android.view.InputEventReceiver; import android.view.InputMonitor; -import android.view.InsetsSource; import android.view.InsetsState; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; import android.view.View; +import android.view.ViewConfiguration; +import android.widget.Toast; +import android.window.TaskSnapshot; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import android.window.flags.DesktopModeFlags; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.jank.Cuj; +import com.android.internal.jank.InteractionJankMonitor; +import com.android.internal.protolog.ProtoLog; import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser; +import com.android.wm.shell.apptoweb.AssistContentRequester; +import com.android.wm.shell.common.DisplayChangeController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; -import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; +import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler; import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition; +import com.android.wm.shell.desktopmode.DesktopTasksLimiter; import com.android.wm.shell.desktopmode.DesktopWallpaperActivity; +import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; -import com.android.wm.shell.shared.DesktopModeStatus; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; +import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreen.StageType; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -100,10 +127,17 @@ import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.ExclusionRegionListener; +import com.android.wm.shell.windowdecor.extension.InsetsStateKt; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; +import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; +import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier; + +import kotlin.Pair; +import kotlin.Unit; import java.io.PrintWriter; -import java.util.Objects; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import java.util.function.Supplier; @@ -123,12 +157,19 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final ShellTaskOrganizer mTaskOrganizer; private final ShellController mShellController; private final Context mContext; - private final Handler mMainHandler; + private final @ShellMainThread Handler mMainHandler; + private final @ShellBackgroundThread ShellExecutor mBgExecutor; private final Choreographer mMainChoreographer; private final DisplayController mDisplayController; private final SyncTransactionQueue mSyncQueue; private final DesktopTasksController mDesktopTasksController; private final InputManager mInputManager; + private final InteractionJankMonitor mInteractionJankMonitor; + private final MultiInstanceHelper mMultiInstanceHelper; + private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository; + private final Optional<DesktopTasksLimiter> mDesktopTasksLimiter; + private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory; + private final WindowDecorViewHostSupplier mWindowDecorViewHostSupplier; private boolean mTransitionDragActive; private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>(); @@ -142,6 +183,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private TaskOperations mTaskOperations; private final Supplier<SurfaceControl.Transaction> mTransactionFactory; private final Transitions mTransitions; + private final Optional<DesktopActivityOrientationChangeHandler> + mActivityOrientationChangeHandler; private SplitScreenController mSplitScreenController; @@ -150,11 +193,14 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final DesktopModeKeyguardChangeListener mDesktopModeKeyguardChangeListener = new DesktopModeKeyguardChangeListener(); private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; + private final AppToWebGenericLinksParser mGenericLinksParser; private final DisplayInsetsController mDisplayInsetsController; private final Region mExclusionRegion = Region.obtain(); private boolean mInImmersiveMode; private final String mSysUIPackageName; + private final AssistContentRequester mAssistContentRequester; + private final DisplayChangeController.OnDisplayChangingListener mOnDisplayChangingListener; private final ISystemGestureExclusionListener mGestureExclusionListener = new ISystemGestureExclusionListener.Stub() { @Override @@ -168,12 +214,14 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { }); } }; + private final TaskPositionerFactory mTaskPositionerFactory; public DesktopModeWindowDecorViewModel( Context context, ShellExecutor shellExecutor, - Handler mainHandler, + @ShellMainThread Handler mainHandler, Choreographer mainChoreographer, + @ShellBackgroundThread ShellExecutor bgExecutor, ShellInit shellInit, ShellCommandHandler shellCommandHandler, IWindowManager windowManager, @@ -184,13 +232,21 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { SyncTransactionQueue syncQueue, Transitions transitions, Optional<DesktopTasksController> desktopTasksController, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer - ) { + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + InteractionJankMonitor interactionJankMonitor, + AppToWebGenericLinksParser genericLinksParser, + AssistContentRequester assistContentRequester, + MultiInstanceHelper multiInstanceHelper, + Optional<DesktopTasksLimiter> desktopTasksLimiter, + WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, + Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler, + WindowDecorViewHostSupplier windowDecorViewHostSupplier) { this( context, shellExecutor, mainHandler, mainChoreographer, + bgExecutor, shellInit, shellCommandHandler, windowManager, @@ -201,19 +257,30 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { syncQueue, transitions, desktopTasksController, + genericLinksParser, + assistContentRequester, + multiInstanceHelper, + windowDecorViewHostSupplier, new DesktopModeWindowDecoration.Factory(), new InputMonitorFactory(), SurfaceControl.Transaction::new, + new AppHeaderViewHolder.Factory(), rootTaskDisplayAreaOrganizer, - new SparseArray<>()); + new SparseArray<>(), + interactionJankMonitor, + desktopTasksLimiter, + windowDecorCaptionHandleRepository, + activityOrientationChangeHandler, + new TaskPositionerFactory()); } @VisibleForTesting DesktopModeWindowDecorViewModel( Context context, ShellExecutor shellExecutor, - Handler mainHandler, + @ShellMainThread Handler mainHandler, Choreographer mainChoreographer, + @ShellBackgroundThread ShellExecutor bgExecutor, ShellInit shellInit, ShellCommandHandler shellCommandHandler, IWindowManager windowManager, @@ -224,15 +291,26 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { SyncTransactionQueue syncQueue, Transitions transitions, Optional<DesktopTasksController> desktopTasksController, + AppToWebGenericLinksParser genericLinksParser, + AssistContentRequester assistContentRequester, + MultiInstanceHelper multiInstanceHelper, + WindowDecorViewHostSupplier windowDecorViewHostSupplier, DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory, InputMonitorFactory inputMonitorFactory, Supplier<SurfaceControl.Transaction> transactionFactory, + AppHeaderViewHolder.Factory appHeaderViewHolderFactory, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, - SparseArray<DesktopModeWindowDecoration> windowDecorByTaskId) { + SparseArray<DesktopModeWindowDecoration> windowDecorByTaskId, + InteractionJankMonitor interactionJankMonitor, + Optional<DesktopTasksLimiter> desktopTasksLimiter, + WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, + Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler, + TaskPositionerFactory taskPositionerFactory) { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; mMainChoreographer = mainChoreographer; + mBgExecutor = bgExecutor; mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class); mTaskOrganizer = taskOrganizer; mShellController = shellController; @@ -241,16 +319,51 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mSyncQueue = syncQueue; mTransitions = transitions; mDesktopTasksController = desktopTasksController.get(); + mMultiInstanceHelper = multiInstanceHelper; mShellCommandHandler = shellCommandHandler; mWindowManager = windowManager; + mWindowDecorViewHostSupplier = windowDecorViewHostSupplier; mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory; mInputMonitorFactory = inputMonitorFactory; mTransactionFactory = transactionFactory; + mAppHeaderViewHolderFactory = appHeaderViewHolderFactory; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; + mGenericLinksParser = genericLinksParser; mInputManager = mContext.getSystemService(InputManager.class); mWindowDecorByTaskId = windowDecorByTaskId; mSysUIPackageName = mContext.getResources().getString( com.android.internal.R.string.config_systemUi); + mInteractionJankMonitor = interactionJankMonitor; + mDesktopTasksLimiter = desktopTasksLimiter; + mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository; + mActivityOrientationChangeHandler = activityOrientationChangeHandler; + mAssistContentRequester = assistContentRequester; + mOnDisplayChangingListener = (displayId, fromRotation, toRotation, displayAreaInfo, t) -> { + DesktopModeWindowDecoration decoration; + RunningTaskInfo taskInfo; + for (int i = 0; i < mWindowDecorByTaskId.size(); i++) { + decoration = mWindowDecorByTaskId.valueAt(i); + if (decoration == null) { + continue; + } else { + taskInfo = decoration.mTaskInfo; + } + + // Check if display has been rotated between portrait & landscape + if (displayId == taskInfo.displayId && taskInfo.isFreeform() + && (fromRotation % 2 != toRotation % 2)) { + // Check if the task bounds on the rotated display will be out of bounds. + // If so, then update task bounds to be within reachable area. + final Rect taskBounds = new Rect( + taskInfo.configuration.windowConfiguration.getBounds()); + if (DragPositioningCallbackUtility.snapTaskBoundsIfNecessary( + taskBounds, decoration.calculateValidDragArea())) { + t.setBounds(taskInfo.token, taskBounds); + } + } + } + }; + mTaskPositionerFactory = taskPositionerFactory; shellInit.addInitCallback(this::onInit, this); } @@ -258,10 +371,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private void onInit() { mShellController.addKeyguardChangeListener(mDesktopModeKeyguardChangeListener); mShellCommandHandler.addDumpCallback(this::dump, this); - mDisplayInsetsController.addInsetsChangedListener(mContext.getDisplayId(), + mDisplayInsetsController.addGlobalInsetsChangedListener( new DesktopModeOnInsetsChangedListener()); mDesktopTasksController.setOnTaskResizeAnimationListener( - new DeskopModeOnTaskResizeAnimationListener()); + new DesktopModeOnTaskResizeAnimationListener()); + mDesktopTasksController.setOnTaskRepositionAnimationListener( + new DesktopModeOnTaskRepositionAnimationListener()); + mDisplayController.addDisplayChangingController(mOnDisplayChangingListener); try { mWindowManager.registerSystemGestureExclusionListener(mGestureExclusionListener, mContext.getDisplayId()); @@ -308,11 +424,14 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { if (decoration == null) return; final RunningTaskInfo oldTaskInfo = decoration.mTaskInfo; - if (taskInfo.displayId != oldTaskInfo.displayId) { + if (taskInfo.displayId != oldTaskInfo.displayId + && !Flags.enableHandleInputFix()) { removeTaskFromEventReceiver(oldTaskInfo.displayId); incrementEventReceiverTasks(taskInfo.displayId); } decoration.relayout(taskInfo); + mActivityOrientationChangeHandler.ifPresent(handler -> + handler.handleActivityOrientationChange(oldTaskInfo, taskInfo)); } @Override @@ -372,7 +491,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { decoration.close(); final int displayId = taskInfo.displayId; - if (mEventReceiversByDisplay.contains(displayId)) { + if (mEventReceiversByDisplay.contains(displayId) + && !Flags.enableHandleInputFix()) { removeTaskFromEventReceiver(displayId); } // Remove the decoration from the cache last because WindowDecoration#close could still @@ -381,17 +501,180 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mWindowDecorByTaskId.remove(taskInfo.taskId); } + private void onMaximizeOrRestore(int taskId, String source) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration == null) { + return; + } + mInteractionJankMonitor.begin( + decoration.mTaskSurface, mContext, mMainHandler, + Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, source); + mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo); + decoration.closeHandleMenu(); + decoration.closeMaximizeMenu(); + } + + private void onSnapResize(int taskId, boolean left) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration == null) { + return; + } + + if (!decoration.mTaskInfo.isResizeable + && DesktopModeFlags.DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE.isTrue()) { + Toast.makeText(mContext, + R.string.desktop_mode_non_resizable_snap_text, Toast.LENGTH_SHORT).show(); + } else { + mInteractionJankMonitor.begin(decoration.mTaskSurface, mContext, mMainHandler, + Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE, "maximize_menu_resizable"); + mDesktopTasksController.snapToHalfScreen( + decoration.mTaskInfo, + decoration.mTaskSurface, + decoration.mTaskInfo.configuration.windowConfiguration.getBounds(), + left ? SnapPosition.LEFT : SnapPosition.RIGHT); + } + + decoration.closeHandleMenu(); + decoration.closeMaximizeMenu(); + } + + private void onOpenInBrowser(int taskId, @NonNull Uri uri) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration == null) { + return; + } + openInBrowser(uri, decoration.getUser()); + decoration.closeHandleMenu(); + decoration.closeMaximizeMenu(); + } + + private void openInBrowser(Uri uri, @NonNull UserHandle userHandle) { + final Intent intent = Intent.makeMainSelectorActivity(ACTION_MAIN, CATEGORY_APP_BROWSER) + .setData(uri) + .addFlags(FLAG_ACTIVITY_NEW_TASK); + mContext.startActivityAsUser(intent, userHandle); + } + + private void onToDesktop(int taskId, DesktopModeTransitionSource source) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration == null) { + return; + } + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mInteractionJankMonitor.begin(decoration.mTaskSurface, mContext, mMainHandler, + CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU); + // App sometimes draws before the insets from WindowDecoration#relayout have + // been added, so they must be added here + decoration.addCaptionInset(wct); + mDesktopTasksController.moveTaskToDesktop(taskId, wct, source); + decoration.closeHandleMenu(); + } + + private void onToFullscreen(int taskId) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration == null) { + return; + } + decoration.closeHandleMenu(); + if (isTaskInSplitScreen(taskId)) { + mSplitScreenController.moveTaskToFullscreen(taskId, + SplitScreenController.EXIT_REASON_DESKTOP_MODE); + } else { + mDesktopTasksController.moveToFullscreen(taskId, + DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON); + } + } + + private void onToSplitScreen(int taskId) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration == null) { + return; + } + decoration.closeHandleMenu(); + // When the app enters split-select, the handle will no longer be visible, meaning + // we shouldn't receive input for it any longer. + decoration.disposeStatusBarInputLayer(); + mDesktopTasksController.requestSplit(decoration.mTaskInfo, false /* leftOrTop */); + } + + private void onNewWindow(int taskId) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration == null) { + return; + } + decoration.closeHandleMenu(); + mDesktopTasksController.openNewWindow(decoration.mTaskInfo); + } + + private void onManageWindows(DesktopModeWindowDecoration decoration) { + if (decoration == null) { + return; + } + decoration.closeHandleMenu(); + decoration.createManageWindowsMenu(getTaskSnapshots(decoration.mTaskInfo), + /* onIconClickListener= */(Integer requestedTaskId) -> { + decoration.closeManageWindowsMenu(); + mDesktopTasksController.openInstance(decoration.mTaskInfo, requestedTaskId); + return Unit.INSTANCE; + }); + } + + private ArrayList<Pair<Integer, TaskSnapshot>> getTaskSnapshots( + @NonNull RunningTaskInfo callerTaskInfo + ) { + final ArrayList<Pair<Integer, TaskSnapshot>> snapshotList = new ArrayList<>(); + final IActivityManager activityManager = ActivityManager.getService(); + final IActivityTaskManager activityTaskManagerService = ActivityTaskManager.getService(); + final List<ActivityManager.RecentTaskInfo> recentTasks; + try { + recentTasks = mActivityTaskManager.getRecentTasks( + Integer.MAX_VALUE, + ActivityManager.RECENT_WITH_EXCLUDED, + activityManager.getCurrentUser().id); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + final String callerPackageName = callerTaskInfo.baseActivity.getPackageName(); + for (ActivityManager.RecentTaskInfo info : recentTasks) { + if (info.taskId == callerTaskInfo.taskId || info.baseActivity == null) continue; + final String infoPackageName = info.baseActivity.getPackageName(); + if (!infoPackageName.equals(callerPackageName)) { + continue; + } + if (info.baseActivity != null) { + if (callerPackageName.equals(infoPackageName)) { + // TODO(b/337903443): Fix this returning null for freeform tasks. + try { + TaskSnapshot screenshot = activityTaskManagerService + .getTaskSnapshot(info.taskId, false); + if (screenshot == null) { + screenshot = activityTaskManagerService + .takeTaskSnapshot(info.taskId, false); + } + snapshotList.add(new Pair(info.taskId, screenshot)); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + } + } + return snapshotList; + } + private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener, View.OnGenericMotionListener, DragDetector.MotionEventHandler { - private static final int CLOSE_MAXIMIZE_MENU_DELAY_MS = 150; + private static final long APP_HANDLE_HOLD_TO_DRAG_DURATION_MS = 100; + private static final long APP_HEADER_HOLD_TO_DRAG_DURATION_MS = 0; private final int mTaskId; private final WindowContainerToken mTaskToken; private final DragPositioningCallback mDragPositioningCallback; - private final DragDetector mDragDetector; + private final DragDetector mHandleDragDetector; + private final DragDetector mHeaderDragDetector; private final GestureDetector mGestureDetector; private final int mDisplayId; + private final Rect mOnDragStartInitialBounds = new Rect(); /** * Whether to pilfer the next motion event to send cancellations to the windows below. @@ -403,7 +686,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private boolean mTouchscreenInUse; private boolean mHasLongClicked; private int mDragPointerId = -1; - private final Runnable mCloseMaximizeWindowRunnable; private DesktopModeTouchEventListener( RunningTaskInfo taskInfo, @@ -411,14 +693,15 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mTaskId = taskInfo.taskId; mTaskToken = taskInfo.token; mDragPositioningCallback = dragPositioningCallback; - mDragDetector = new DragDetector(this); + final int touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); + final long appHandleHoldToDragDuration = Flags.enableHoldToDragAppHandle() + ? APP_HANDLE_HOLD_TO_DRAG_DURATION_MS : 0; + mHandleDragDetector = new DragDetector(this, appHandleHoldToDragDuration, + touchSlop); + mHeaderDragDetector = new DragDetector(this, APP_HEADER_HOLD_TO_DRAG_DURATION_MS, + touchSlop); mGestureDetector = new GestureDetector(mContext, this); mDisplayId = taskInfo.displayId; - mCloseMaximizeWindowRunnable = () -> { - final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); - if (decoration == null) return; - decoration.closeMaximizeMenu(); - }; } @Override @@ -435,7 +718,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { SplitScreenController.EXIT_REASON_DESKTOP_MODE); } else { WindowContainerTransaction wct = new WindowContainerTransaction(); - mDesktopTasksController.onDesktopWindowClose(wct, mTaskId); + mDesktopTasksController.onDesktopWindowClose(wct, mDisplayId, mTaskId); mTaskOperations.closeTask(mTaskToken, wct); } } else if (id == R.id.back_button) { @@ -443,75 +726,53 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } else if (id == R.id.caption_handle || id == R.id.open_menu_button) { if (!decoration.isHandleMenuActive()) { moveTaskToFront(decoration.mTaskInfo); - decoration.createHandleMenu(mSplitScreenController); - } else { - decoration.closeHandleMenu(); + decoration.createHandleMenu(checkNumberOfOtherInstances(decoration.mTaskInfo) + >= MANAGE_WINDOWS_MINIMUM_INSTANCES); } - } else if (id == R.id.desktop_button) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - // App sometimes draws before the insets from WindowDecoration#relayout have - // been added, so they must be added here - mWindowDecorByTaskId.get(mTaskId).addCaptionInset(wct); - mDesktopTasksController.moveToDesktop(mTaskId, wct, - DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON); - decoration.closeHandleMenu(); - } else if (id == R.id.fullscreen_button) { - decoration.closeHandleMenu(); - if (isTaskInSplitScreen(mTaskId)) { - mSplitScreenController.moveTaskToFullscreen(mTaskId, - SplitScreenController.EXIT_REASON_DESKTOP_MODE); - } else { - mDesktopTasksController.moveToFullscreen(mTaskId, - DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON); - } - } else if (id == R.id.split_screen_button) { - decoration.closeHandleMenu(); - mDesktopTasksController.requestSplit(decoration.mTaskInfo); - } else if (id == R.id.collapse_menu_button) { - decoration.closeHandleMenu(); } else if (id == R.id.maximize_window) { - final RunningTaskInfo taskInfo = decoration.mTaskInfo; - decoration.closeHandleMenu(); - decoration.closeMaximizeMenu(); - mDesktopTasksController.toggleDesktopTaskSize(taskInfo); - } else if (id == R.id.maximize_menu_maximize_button) { - final RunningTaskInfo taskInfo = decoration.mTaskInfo; - mDesktopTasksController.toggleDesktopTaskSize(taskInfo); - decoration.closeHandleMenu(); - decoration.closeMaximizeMenu(); - } else if (id == R.id.maximize_menu_snap_left_button) { - final RunningTaskInfo taskInfo = decoration.mTaskInfo; - mDesktopTasksController.snapToHalfScreen(taskInfo, SnapPosition.LEFT); - decoration.closeHandleMenu(); - decoration.closeMaximizeMenu(); - } else if (id == R.id.maximize_menu_snap_right_button) { - final RunningTaskInfo taskInfo = decoration.mTaskInfo; - mDesktopTasksController.snapToHalfScreen(taskInfo, SnapPosition.RIGHT); - decoration.closeHandleMenu(); - decoration.closeMaximizeMenu(); + // TODO(b/346441962): move click detection logic into the decor's + // {@link AppHeaderViewHolder}. Let it encapsulate the that and have it report + // back to the decoration using + // {@link DesktopModeWindowDecoration#setOnMaximizeOrRestoreClickListener}, which + // should shared with the maximize menu's maximize/restore actions. + onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button"); + } else if (id == R.id.minimize_window) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mDesktopTasksController.onDesktopWindowMinimize(wct, mTaskId); + final IBinder transition = mTaskOperations.minimizeTask(mTaskToken, wct); + mDesktopTasksLimiter.ifPresent(limiter -> + limiter.addPendingMinimizeChange(transition, mDisplayId, mTaskId)); } } @Override public boolean onTouch(View v, MotionEvent e) { final int id = v.getId(); + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); if ((e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN) { mTouchscreenInUse = e.getActionMasked() != ACTION_UP && e.getActionMasked() != ACTION_CANCEL; } if (id != R.id.caption_handle && id != R.id.desktop_mode_caption && id != R.id.open_menu_button && id != R.id.close_window - && id != R.id.maximize_window) { + && id != R.id.maximize_window && id != R.id.minimize_window) { return false; } - final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); - moveTaskToFront(decoration.mTaskInfo); - + final boolean isAppHandle = !getTaskInfo().isFreeform(); final int actionMasked = e.getActionMasked(); final boolean isDown = actionMasked == MotionEvent.ACTION_DOWN; final boolean isUpOrCancel = actionMasked == MotionEvent.ACTION_CANCEL || actionMasked == MotionEvent.ACTION_UP; if (isDown) { + // Only move to front on down to prevent 2+ tasks from fighting + // (and thus flickering) for front status when drag-moving them simultaneously with + // two pointers. + // TODO(b/356962065): during a drag-move, this shouldn't be a WCT - just move the + // task surface to the top of other tasks and reorder once the user releases the + // gesture together with the bounds' WCT. This is probably still valid for other + // gestures like simple clicks. + moveTaskToFront(decoration.mTaskInfo); + final boolean downInCustomizableCaptionRegion = decoration.checkTouchEventInCustomizableRegion(e); final boolean downInExclusionRegion = mExclusionRegion.contains( @@ -533,7 +794,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mShouldPilferCaptionEvents = !(downInCustomizableCaptionRegion && downInExclusionRegion && isTransparentCaption) && !isResizeEvent; } - if (!mShouldPilferCaptionEvents) { // The event will be handled by a window below or pilfered by resize handler. return false; @@ -547,10 +807,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { // Gesture is finished, reset state. mShouldPilferCaptionEvents = false; } - if (!mHasLongClicked && id != R.id.maximize_window) { - decoration.closeMaximizeMenuIfNeeded(e); + if (isAppHandle) { + return mHandleDragDetector.onMotionEvent(v, e); + } else { + return mHeaderDragDetector.onMotionEvent(v, e); } - return mDragDetector.onMotionEvent(v, e); } @Override @@ -570,40 +831,26 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { return false; } + /** + * TODO(b/346441962): move this hover detection logic into the decor's + * {@link AppHeaderViewHolder}. + */ @Override public boolean onGenericMotion(View v, MotionEvent ev) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); final int id = v.getId(); - if (ev.getAction() == ACTION_HOVER_ENTER) { - if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) { - decoration.onMaximizeWindowHoverEnter(); - } else if (id == R.id.maximize_window - || MaximizeMenu.Companion.isMaximizeMenuView(id)) { - // Re-hovering over any of the maximize menu views should keep the menu open by - // cancelling any attempts to close the menu. - mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable); - if (id != R.id.maximize_window) { - decoration.onMaximizeMenuHoverEnter(id, ev); - } + if (ev.getAction() == ACTION_HOVER_ENTER && id == R.id.maximize_window) { + decoration.setAppHeaderMaximizeButtonHovered(true); + if (!decoration.isMaximizeMenuActive()) { + decoration.onMaximizeButtonHoverEnter(); } return true; - } else if (ev.getAction() == ACTION_HOVER_MOVE - && MaximizeMenu.Companion.isMaximizeMenuView(id)) { - decoration.onMaximizeMenuHoverMove(id, ev); - mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable); - } else if (ev.getAction() == ACTION_HOVER_EXIT) { - if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) { - decoration.onMaximizeWindowHoverExit(); - } else if (id == R.id.maximize_window - || MaximizeMenu.Companion.isMaximizeMenuView(id)) { - // Close menu if not hovering over maximize menu or maximize button after a - // delay to give user a chance to re-enter view or to move from one maximize - // menu view to another. - mMainHandler.postDelayed(mCloseMaximizeWindowRunnable, - CLOSE_MAXIMIZE_MENU_DELAY_MS); - if (id != R.id.maximize_window) { - decoration.onMaximizeMenuHoverExit(id, ev); - } + } + if (ev.getAction() == ACTION_HOVER_EXIT && id == R.id.maximize_window) { + decoration.setAppHeaderMaximizeButtonHovered(false); + decoration.onMaximizeHoverStateChanged(); + if (!decoration.isMaximizeMenuActive()) { + decoration.onMaximizeButtonHoverExit(); } return true; } @@ -625,22 +872,57 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); final RunningTaskInfo taskInfo = decoration.mTaskInfo; if (DesktopModeStatus.canEnterDesktopMode(mContext) - && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { - return false; + && !taskInfo.isFreeform()) { + return handleNonFreeformMotionEvent(decoration, v, e); + } else { + return handleFreeformMotionEvent(decoration, taskInfo, v, e); + } + } + + @NonNull + private RunningTaskInfo getTaskInfo() { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); + return decoration.mTaskInfo; + } + + private boolean handleNonFreeformMotionEvent(DesktopModeWindowDecoration decoration, + View v, MotionEvent e) { + final int id = v.getId(); + if (id == R.id.caption_handle) { + handleCaptionThroughStatusBar(e, decoration); + final boolean wasDragging = mIsDragging; + updateDragStatus(e.getActionMasked()); + final boolean upOrCancel = e.getActionMasked() == ACTION_UP + || e.getActionMasked() == ACTION_CANCEL; + if (wasDragging && upOrCancel) { + // When finishing a drag the event will be consumed, which means the pressed + // state of the App Handle must be manually reset to scale its drawable back to + // its original shape. This is necessary for drag gestures of the Handle that + // result in a cancellation (dragging back to the top). + v.setPressed(false); + } + // Only prevent onClick from receiving this event if it's a drag. + return wasDragging; } + return false; + } + + private boolean handleFreeformMotionEvent(DesktopModeWindowDecoration decoration, + RunningTaskInfo taskInfo, View v, MotionEvent e) { + final int id = v.getId(); if (mGestureDetector.onTouchEvent(e)) { return true; } - final int id = v.getId(); final boolean touchingButton = (id == R.id.close_window || id == R.id.maximize_window - || id == R.id.open_menu_button); + || id == R.id.open_menu_button || id == R.id.minimize_window); switch (e.getActionMasked()) { case MotionEvent.ACTION_DOWN: { mDragPointerId = e.getPointerId(0); - mDragPositioningCallback.onDragPositioningStart( + final Rect initialBounds = mDragPositioningCallback.onDragPositioningStart( 0 /* ctrlType */, e.getRawX(0), e.getRawY(0)); - mIsDragging = false; + updateDragStatus(e.getActionMasked()); + mOnDragStartInitialBounds.set(initialBounds); mHasLongClicked = false; // Do not consume input event if a button is touched, otherwise it would // prevent the button's ripple effect from showing. @@ -660,7 +942,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { decoration.mTaskSurface, e.getRawX(dragPointerIdx), newTaskBounds); - mIsDragging = true; + // Flip mIsDragging only if the bounds actually changed. + if (mIsDragging || !newTaskBounds.equals(mOnDragStartInitialBounds)) { + updateDragStatus(e.getActionMasked()); + } return true; } case MotionEvent.ACTION_UP: @@ -681,16 +966,21 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { (int) (e.getRawY(dragPointerIdx) - e.getY(dragPointerIdx))); final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningEnd( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); - mDesktopTasksController.onDragPositioningEnd(taskInfo, position, + // Tasks bounds haven't actually been updated (only its leash), so pass to + // DesktopTasksController to allow secondary transformations (i.e. snap resizing + // or transforming to fullscreen) before setting new task bounds. + mDesktopTasksController.onDragPositioningEnd( + taskInfo, decoration.mTaskSurface, position, new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)), - newTaskBounds, decoration.calculateValidDragArea()); + newTaskBounds, decoration.calculateValidDragArea(), + new Rect(mOnDragStartInitialBounds)); if (touchingButton && !mHasLongClicked) { // We need the input event to not be consumed here to end the ripple // effect on the touched button. We will reset drag state in the ensuing // onClick call that results. return false; } else { - mIsDragging = false; + updateDragStatus(e.getActionMasked()); return true; } } @@ -698,6 +988,21 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { return true; } + private void updateDragStatus(int eventAction) { + switch (eventAction) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + mIsDragging = false; + break; + } + case MotionEvent.ACTION_MOVE: { + mIsDragging = true; + break; + } + } + } + /** * Perform a task size toggle on release of the double-tap, assuming no drag event * was handled during the double-tap. @@ -711,8 +1016,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && action != MotionEvent.ACTION_CANCEL)) { return false; } - final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); - mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo); + onMaximizeOrRestore(mTaskId, "double_tap"); return true; } } @@ -833,9 +1137,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { relevantDecor.updateHoverAndPressStatus(ev); final int action = ev.getActionMasked(); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { - if (!mTransitionDragActive) { + if (!mTransitionDragActive && !Flags.enableHandleInputFix()) { relevantDecor.closeHandleMenuIfNeeded(ev); - relevantDecor.closeMaximizeMenuIfNeeded(ev); } } } @@ -875,24 +1178,55 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN || windowingMode == WINDOWING_MODE_MULTI_WINDOW; } - - if (dragFromStatusBarAllowed - && relevantDecor.checkTouchEventInFocusedCaptionHandle(ev)) { + final boolean shouldStartTransitionDrag = + relevantDecor.checkTouchEventInFocusedCaptionHandle(ev) + || Flags.enableHandleInputFix(); + if (dragFromStatusBarAllowed && shouldStartTransitionDrag) { mTransitionDragActive = true; } break; } case MotionEvent.ACTION_UP: { if (mTransitionDragActive) { + final DesktopModeVisualIndicator.DragStartState dragStartState = + DesktopModeVisualIndicator.DragStartState + .getDragStartState(relevantDecor.mTaskInfo); + if (dragStartState == null) return; mDesktopTasksController.updateVisualIndicator(relevantDecor.mTaskInfo, - relevantDecor.mTaskSurface, ev.getRawX(), ev.getRawY()); + relevantDecor.mTaskSurface, ev.getRawX(), ev.getRawY(), + dragStartState); mTransitionDragActive = false; if (mMoveToDesktopAnimator != null) { // Though this isn't a hover event, we need to update handle's hover state // as it likely will change. relevantDecor.updateHoverAndPressStatus(ev); - mDesktopTasksController.onDragPositioningEndThroughStatusBar( - new PointF(ev.getRawX(), ev.getRawY()), relevantDecor.mTaskInfo); + DesktopModeVisualIndicator.IndicatorType resultType = + mDesktopTasksController.onDragPositioningEndThroughStatusBar( + new PointF(ev.getRawX(), ev.getRawY()), + relevantDecor.mTaskInfo, + relevantDecor.mTaskSurface); + // If we are entering split select, handle will no longer be visible and + // should not be receiving any input. + if (resultType == TO_SPLIT_LEFT_INDICATOR + || resultType == TO_SPLIT_RIGHT_INDICATOR) { + relevantDecor.disposeStatusBarInputLayer(); + // We should also dispose the other split task's input layer if + // applicable. + final int splitPosition = mSplitScreenController + .getSplitPosition(relevantDecor.mTaskInfo.taskId); + if (splitPosition != SPLIT_POSITION_UNDEFINED) { + final int oppositePosition = + splitPosition == SPLIT_POSITION_TOP_OR_LEFT + ? SPLIT_POSITION_BOTTOM_OR_RIGHT + : SPLIT_POSITION_TOP_OR_LEFT; + final RunningTaskInfo oppositeTaskInfo = + mSplitScreenController.getTaskInfo(oppositePosition); + if (oppositeTaskInfo != null) { + mWindowDecorByTaskId.get(oppositeTaskInfo.taskId) + .disposeStatusBarInputLayer(); + } + } + } mMoveToDesktopAnimator = null; return; } else { @@ -904,7 +1238,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { relevantDecor.checkTouchEvent(ev); break; } - case ACTION_MOVE: { if (relevantDecor == null) { return; @@ -917,17 +1250,22 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && mMoveToDesktopAnimator == null) { return; } + final DesktopModeVisualIndicator.DragStartState dragStartState = + DesktopModeVisualIndicator.DragStartState + .getDragStartState(relevantDecor.mTaskInfo); + if (dragStartState == null) return; final DesktopModeVisualIndicator.IndicatorType indicatorType = mDesktopTasksController.updateVisualIndicator( relevantDecor.mTaskInfo, - relevantDecor.mTaskSurface, ev.getRawX(), ev.getRawY()); + relevantDecor.mTaskSurface, ev.getRawX(), ev.getRawY(), + dragStartState); if (indicatorType != TO_FULLSCREEN_INDICATOR) { if (mMoveToDesktopAnimator == null) { mMoveToDesktopAnimator = new MoveToDesktopAnimator( mContext, mDragToDesktopAnimationStartBounds, relevantDecor.mTaskInfo, relevantDecor.mTaskSurface); mDesktopTasksController.startDragToDesktop(relevantDecor.mTaskInfo, - mMoveToDesktopAnimator); + mMoveToDesktopAnimator, relevantDecor.mTaskSurface); } } if (mMoveToDesktopAnimator != null) { @@ -1016,7 +1354,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private void createInputChannel(int displayId) { final InputManager inputManager = mContext.getSystemService(InputManager.class); final InputMonitor inputMonitor = - mInputMonitorFactory.create(inputManager, mContext); + mInputMonitorFactory.create(inputManager, displayId); final EventReceiver eventReceiver = new EventReceiver(inputMonitor, inputMonitor.getInputChannel(), Looper.myLooper()); mEventReceiversByDisplay.put(displayId, eventReceiver); @@ -1035,16 +1373,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && mSplitScreenController.isTaskRootOrStageRoot(taskInfo.taskId)) { return false; } - if (mDesktopModeKeyguardChangeListener.isKeyguardVisibleAndOccluded() - && taskInfo.isFocused) { - return false; - } - // TODO(b/347289970): Consider replacing with API - if (Flags.enableDesktopWindowingModalsPolicy() - && isSingleTopActivityTranslucent(taskInfo)) { - return false; - } - if (isSystemUIApplication(taskInfo)) { + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() + && isTopActivityExemptFromDesktopWindowing(mContext, taskInfo)) { return false; } return DesktopModeStatus.canEnterDesktopMode(mContext) @@ -1067,42 +1397,81 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { final DesktopModeWindowDecoration windowDecoration = mDesktopModeWindowDecorFactory.create( mContext, + mContext.createContextAsUser(UserHandle.of(taskInfo.userId), 0 /* flags */), mDisplayController, + mSplitScreenController, mTaskOrganizer, taskInfo, taskSurface, mMainHandler, + mBgExecutor, mMainChoreographer, mSyncQueue, - mRootTaskDisplayAreaOrganizer); + mAppHeaderViewHolderFactory, + mRootTaskDisplayAreaOrganizer, + mGenericLinksParser, + mAssistContentRequester, + mMultiInstanceHelper, + mWindowDecorCaptionHandleRepository, + mWindowDecorViewHostSupplier); mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); - final DragPositioningCallback dragPositioningCallback; - if (!DesktopModeStatus.isVeiledResizeEnabled()) { - dragPositioningCallback = new FluidResizeTaskPositioner( - mTaskOrganizer, mTransitions, windowDecoration, mDisplayController, - mDragStartListener, mTransactionFactory); - windowDecoration.setTaskDragResizer( - (FluidResizeTaskPositioner) dragPositioningCallback); - } else { - dragPositioningCallback = new VeiledResizeTaskPositioner( - mTaskOrganizer, windowDecoration, mDisplayController, - mDragStartListener, mTransitions); - windowDecoration.setTaskDragResizer( - (VeiledResizeTaskPositioner) dragPositioningCallback); - } + final TaskPositioner taskPositioner = mTaskPositionerFactory.create( + mTaskOrganizer, + windowDecoration, + mDisplayController, + mDragStartListener, + mTransitions, + mInteractionJankMonitor, + mTransactionFactory, + mMainHandler); + windowDecoration.setTaskDragResizer(taskPositioner); final DesktopModeTouchEventListener touchEventListener = - new DesktopModeTouchEventListener(taskInfo, dragPositioningCallback); - + new DesktopModeTouchEventListener(taskInfo, taskPositioner); + windowDecoration.setOnMaximizeOrRestoreClickListener(() -> { + onMaximizeOrRestore(taskInfo.taskId, "maximize_menu"); + return Unit.INSTANCE; + }); + windowDecoration.setOnLeftSnapClickListener(() -> { + onSnapResize(taskInfo.taskId, true /* isLeft */); + return Unit.INSTANCE; + }); + windowDecoration.setOnRightSnapClickListener(() -> { + onSnapResize(taskInfo.taskId, false /* isLeft */); + return Unit.INSTANCE; + }); + windowDecoration.setOnToDesktopClickListener(desktopModeTransitionSource -> { + onToDesktop(taskInfo.taskId, desktopModeTransitionSource); + }); + windowDecoration.setOnToFullscreenClickListener(() -> { + onToFullscreen(taskInfo.taskId); + return Unit.INSTANCE; + }); + windowDecoration.setOnToSplitScreenClickListener(() -> { + onToSplitScreen(taskInfo.taskId); + return Unit.INSTANCE; + }); + windowDecoration.setOpenInBrowserClickListener((uri) -> { + onOpenInBrowser(taskInfo.taskId, uri); + }); + windowDecoration.setOnNewWindowClickListener(() -> { + onNewWindow(taskInfo.taskId); + return Unit.INSTANCE; + }); + windowDecoration.setManageWindowsClickListener(() -> { + onManageWindows(windowDecoration); + return Unit.INSTANCE; + }); windowDecoration.setCaptionListeners( touchEventListener, touchEventListener, touchEventListener, touchEventListener); windowDecoration.setExclusionRegionListener(mExclusionRegionListener); - windowDecoration.setDragPositioningCallback(dragPositioningCallback); - windowDecoration.setDragDetector(touchEventListener.mDragDetector); + windowDecoration.setDragPositioningCallback(taskPositioner); windowDecoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */); - incrementEventReceiverTasks(taskInfo.displayId); + if (!Flags.enableHandleInputFix()) { + incrementEventReceiverTasks(taskInfo.displayId); + } } private RunningTaskInfo getOtherSplitTask(int taskId) { @@ -1117,14 +1486,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && mSplitScreenController.isTaskInSplitScreen(taskId); } - // TODO(b/347289970): Consider replacing with API - private boolean isSystemUIApplication(RunningTaskInfo taskInfo) { - if (taskInfo.baseActivity != null) { - return (Objects.equals(taskInfo.baseActivity.getPackageName(), mSysUIPackageName)); - } - return false; - } - private void dump(PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; pw.println(prefix + "DesktopModeWindowDecorViewModel"); @@ -1135,7 +1496,26 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { pw.println(innerPrefix + "mWindowDecorByTaskId=" + mWindowDecorByTaskId); } - private class DeskopModeOnTaskResizeAnimationListener + private class DesktopModeOnTaskRepositionAnimationListener + implements OnTaskRepositionAnimationListener { + @Override + public void onAnimationStart(int taskId) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration != null) { + decoration.setAnimatingTaskResizeOrReposition(true); + } + } + + @Override + public void onAnimationEnd(int taskId) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration != null) { + decoration.setAnimatingTaskResizeOrReposition(false); + } + } + } + + private class DesktopModeOnTaskResizeAnimationListener implements OnTaskResizeAnimationListener { @Override public void onAnimationStart(int taskId, Transaction t, Rect bounds) { @@ -1145,7 +1525,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { return; } decoration.showResizeVeil(t, bounds); - decoration.setAnimatingTaskResize(true); + decoration.setAnimatingTaskResizeOrReposition(true); } @Override @@ -1160,11 +1540,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); if (decoration == null) return; decoration.hideResizeVeil(); - decoration.setAnimatingTaskResize(false); + decoration.setAnimatingTaskResizeOrReposition(false); } } - private class DragStartListenerImpl implements DragPositioningCallbackUtility.DragStartListener { @Override @@ -1174,9 +1553,32 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } } + /** + * Gets the number of instances of a task running, not including the specified task itself. + */ + private int checkNumberOfOtherInstances(@NonNull RunningTaskInfo info) { + // TODO(b/336289597): Rather than returning number of instances, return a list of valid + // instances, then refer to the list's size and reuse the list for Manage Windows menu. + final IActivityTaskManager activityTaskManager = ActivityTaskManager.getService(); + final IActivityManager activityManager = ActivityManager.getService(); + try { + return activityTaskManager.getRecentTasks(Integer.MAX_VALUE, + ActivityManager.RECENT_WITH_EXCLUDED, + activityManager.getCurrentUserId()).getList().stream().filter( + recentTaskInfo -> (recentTaskInfo.taskId != info.taskId + && recentTaskInfo.baseActivity != null + && recentTaskInfo.baseActivity.getPackageName() + .equals(info.baseActivity.getPackageName()) + ) + ).toList().size(); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + static class InputMonitorFactory { - InputMonitor create(InputManager inputManager, Context context) { - return inputManager.monitorGestureInput("caption-touch", context.getDisplayId()); + InputMonitor create(InputManager inputManager, int displayId) { + return inputManager.monitorGestureInput("caption-touch", displayId); } } @@ -1194,19 +1596,17 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } } - static class DesktopModeKeyguardChangeListener implements KeyguardChangeListener { - private boolean mIsKeyguardVisible; - private boolean mIsKeyguardOccluded; - + class DesktopModeKeyguardChangeListener implements KeyguardChangeListener { @Override public void onKeyguardVisibilityChanged(boolean visible, boolean occluded, boolean animatingDismiss) { - mIsKeyguardVisible = visible; - mIsKeyguardOccluded = occluded; - } - - public boolean isKeyguardVisibleAndOccluded() { - return mIsKeyguardVisible && mIsKeyguardOccluded; + final int size = mWindowDecorByTaskId.size(); + for (int i = size - 1; i >= 0; i--) { + final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i); + if (decor != null) { + decor.onKeyguardStateChanged(visible, occluded); + } + } } } @@ -1214,31 +1614,63 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { class DesktopModeOnInsetsChangedListener implements DisplayInsetsController.OnInsetsChangedListener { @Override - public void insetsChanged(InsetsState insetsState) { - for (int i = 0; i < insetsState.sourceSize(); i++) { - final InsetsSource source = insetsState.sourceAt(i); - if (source.getType() != statusBars()) { + public void insetsChanged(int displayId, @NonNull InsetsState insetsState) { + final int size = mWindowDecorByTaskId.size(); + for (int i = size - 1; i >= 0; i--) { + final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i); + if (decor == null) { continue; } - - final DesktopModeWindowDecoration decor = getFocusedDecor(); - if (decor == null) { - return; + if (decor.mTaskInfo.displayId == displayId + && Flags.enableDesktopWindowingImmersiveHandleHiding()) { + decor.onInsetsStateChanged(insetsState); } - // If status bar inset is visible, top task is not in immersive mode - final boolean inImmersiveMode = !source.isVisible(); - // Calls WindowDecoration#relayout if decoration visibility needs to be updated - if (inImmersiveMode != mInImmersiveMode) { - if (Flags.enableDesktopWindowingImmersiveHandleHiding()) { - decor.relayout(decor.mTaskInfo); - } - mInImmersiveMode = inImmersiveMode; + if (!Flags.enableHandleInputFix()) { + // If status bar inset is visible, top task is not in immersive mode. + // This value is only needed when the App Handle input is being handled + // through the global input monitor (hence the flag check) to ignore gestures + // when the app is in immersive mode. When disabled, the view itself handles + // input, and since it's removed when in immersive there's no need to track + // this here. + mInImmersiveMode = !InsetsStateKt.isVisible(insetsState, statusBars()); } + } + } + } - return; + @VisibleForTesting + static class TaskPositionerFactory { + TaskPositioner create( + ShellTaskOrganizer taskOrganizer, + DesktopModeWindowDecoration windowDecoration, + DisplayController displayController, + DragPositioningCallbackUtility.DragStartListener dragStartListener, + Transitions transitions, + InteractionJankMonitor interactionJankMonitor, + Supplier<SurfaceControl.Transaction> transactionFactory, + Handler handler) { + final TaskPositioner taskPositioner = DesktopModeStatus.isVeiledResizeEnabled() + ? new VeiledResizeTaskPositioner( + taskOrganizer, + windowDecoration, + displayController, + dragStartListener, + transitions, + interactionJankMonitor, + handler) + : new FluidResizeTaskPositioner( + taskOrganizer, + transitions, + windowDecoration, + displayController, + dragStartListener, + transactionFactory); + + if (DesktopModeFlags.ENABLE_WINDOWING_SCALED_RESIZING.isTrue()) { + return new FixedAspectRatioTaskPositionerDecorator(windowDecoration, + taskPositioner); } + return taskPositioner; } } } - - diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 4d597cac889e..23f8e6ef1596 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -19,18 +19,29 @@ package com.android.wm.shell.windowdecor; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.windowingModeToString; +import static android.view.InsetsSource.FLAG_FORCE_CONSUMING; +import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR; import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_UP; +import static android.window.flags.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION; +import static android.window.flags.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS; import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT; +import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode; +import static com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; +import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeHandleEdgeInset; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.WindowConfiguration.WindowingMode; +import android.app.assist.AssistContent; +import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; @@ -43,10 +54,12 @@ import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.Drawable; +import android.net.Uri; import android.os.Handler; import android.os.Trace; -import android.util.Log; +import android.os.UserHandle; import android.util.Size; +import android.util.Slog; import android.view.Choreographer; import android.view.MotionEvent; import android.view.SurfaceControl; @@ -54,7 +67,9 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.WindowManager; import android.widget.ImageButton; +import android.window.TaskSnapshot; import android.window.WindowContainerTransaction; +import android.window.flags.DesktopModeFlags; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.ScreenDecorationsUtils; @@ -64,18 +79,34 @@ import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser; +import com.android.wm.shell.apptoweb.AppToWebUtils; +import com.android.wm.shell.apptoweb.AssistContentRequester; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.MultiInstanceHelper; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.shared.DesktopModeStatus; +import com.android.wm.shell.desktopmode.CaptionState; +import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; +import com.android.wm.shell.shared.desktopmode.ManageWindowsViewContainer; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; import com.android.wm.shell.windowdecor.viewholder.AppHandleViewHolder; import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; import com.android.wm.shell.windowdecor.viewholder.WindowDecorationViewHolder; +import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier; +import kotlin.Pair; import kotlin.Unit; +import kotlin.jvm.functions.Function0; +import kotlin.jvm.functions.Function1; +import java.util.List; +import java.util.function.Consumer; import java.util.function.Supplier; /** @@ -86,28 +117,41 @@ import java.util.function.Supplier; */ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { private static final String TAG = "DesktopModeWindowDecoration"; + private static final int CAPTURED_LINK_TIMEOUT_MS = 7000; + + @VisibleForTesting + static final long CLOSE_MAXIMIZE_MENU_DELAY_MS = 150L; private final Handler mHandler; + private final @ShellBackgroundThread ShellExecutor mBgExecutor; private final Choreographer mChoreographer; private final SyncTransactionQueue mSyncQueue; + private final SplitScreenController mSplitScreenController; + private final WindowManagerWrapper mWindowManagerWrapper; private WindowDecorationViewHolder mWindowDecorViewHolder; private View.OnClickListener mOnCaptionButtonClickListener; private View.OnTouchListener mOnCaptionTouchListener; private View.OnLongClickListener mOnCaptionLongClickListener; private View.OnGenericMotionListener mOnCaptionGenericMotionListener; + private Function0<Unit> mOnMaximizeOrRestoreClickListener; + private Function0<Unit> mOnLeftSnapClickListener; + private Function0<Unit> mOnRightSnapClickListener; + private Consumer<DesktopModeTransitionSource> mOnToDesktopClickListener; + private Function0<Unit> mOnToFullscreenClickListener; + private Function0<Unit> mOnToSplitscreenClickListener; + private Function0<Unit> mOnNewWindowClickListener; + private Function0<Unit> mOnManageWindowsClickListener; private DragPositioningCallback mDragPositioningCallback; private DragResizeInputListener mDragResizeListener; - private DragDetector mDragDetector; - private Runnable mCurrentViewHostRunnable = null; private RelayoutParams mRelayoutParams = new RelayoutParams(); private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult = new WindowDecoration.RelayoutResult<>(); - private final Runnable mViewHostRunnable = - () -> updateViewHost(mRelayoutParams, null /* onDrawTransaction */, mResult); private final Point mPositionInParent = new Point(); private HandleMenu mHandleMenu; + private boolean mMinimumInstancesFound; + private ManageWindowsViewContainer mManageWindowsMenu; private MaximizeMenu mMaximizeMenu; @@ -116,51 +160,159 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private Bitmap mResizeVeilBitmap; private CharSequence mAppName; + private CapturedLink mCapturedLink; + private Uri mGenericLink; + private Uri mWebUri; + private Consumer<Uri> mOpenInBrowserClickListener; private ExclusionRegionListener mExclusionRegionListener; + private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory; private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; + private final MaximizeMenuFactory mMaximizeMenuFactory; + private final HandleMenuFactory mHandleMenuFactory; + private final AppToWebGenericLinksParser mGenericLinksParser; + private final AssistContentRequester mAssistContentRequester; + + // Hover state for the maximize menu and button. The menu will remain open as long as either of + // these is true. See {@link #onMaximizeHoverStateChanged()}. + private boolean mIsAppHeaderMaximizeButtonHovered = false; + private boolean mIsMaximizeMenuHovered = false; + // Used to schedule the closing of the maximize menu when neither of the button or menu are + // being hovered. There's a small delay after stopping the hover, to allow a quick reentry + // to cancel the close. + private final Runnable mCloseMaximizeWindowRunnable = this::closeMaximizeMenu; + private final Runnable mCapturedLinkExpiredRunnable = this::onCapturedLinkExpired; + private final MultiInstanceHelper mMultiInstanceHelper; + private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository; DesktopModeWindowDecoration( Context context, + @NonNull Context userContext, DisplayController displayController, + SplitScreenController splitScreenController, ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, Handler handler, + @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { - this (context, displayController, taskOrganizer, taskInfo, taskSurface, - handler, choreographer, syncQueue, rootTaskDisplayAreaOrganizer, + AppHeaderViewHolder.Factory appHeaderViewHolderFactory, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + AppToWebGenericLinksParser genericLinksParser, + AssistContentRequester assistContentRequester, + MultiInstanceHelper multiInstanceHelper, + WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, + WindowDecorViewHostSupplier windowDecorViewHostSupplier) { + this (context, userContext, displayController, splitScreenController, taskOrganizer, + taskInfo, taskSurface, handler, bgExecutor, choreographer, syncQueue, + appHeaderViewHolderFactory, rootTaskDisplayAreaOrganizer, genericLinksParser, + assistContentRequester, SurfaceControl.Builder::new, SurfaceControl.Transaction::new, - WindowContainerTransaction::new, SurfaceControl::new, - new SurfaceControlViewHostFactory() {}); + WindowContainerTransaction::new, SurfaceControl::new, new WindowManagerWrapper( + context.getSystemService(WindowManager.class)), + new SurfaceControlViewHostFactory() {}, windowDecorViewHostSupplier, + DefaultMaximizeMenuFactory.INSTANCE, + DefaultHandleMenuFactory.INSTANCE, multiInstanceHelper, + windowDecorCaptionHandleRepository); } DesktopModeWindowDecoration( Context context, + @NonNull Context userContext, DisplayController displayController, + SplitScreenController splitScreenController, ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, Handler handler, + @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, + AppHeaderViewHolder.Factory appHeaderViewHolderFactory, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + AppToWebGenericLinksParser genericLinksParser, + AssistContentRequester assistContentRequester, Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, Supplier<WindowContainerTransaction> windowContainerTransactionSupplier, Supplier<SurfaceControl> surfaceControlSupplier, - SurfaceControlViewHostFactory surfaceControlViewHostFactory) { - super(context, displayController, taskOrganizer, taskInfo, taskSurface, + WindowManagerWrapper windowManagerWrapper, + SurfaceControlViewHostFactory surfaceControlViewHostFactory, + WindowDecorViewHostSupplier windowDecorViewHostSupplier, + MaximizeMenuFactory maximizeMenuFactory, + HandleMenuFactory handleMenuFactory, + MultiInstanceHelper multiInstanceHelper, + WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository) { + super(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface, surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, windowContainerTransactionSupplier, surfaceControlSupplier, - surfaceControlViewHostFactory); + surfaceControlViewHostFactory, windowDecorViewHostSupplier); + mSplitScreenController = splitScreenController; mHandler = handler; + mBgExecutor = bgExecutor; mChoreographer = choreographer; mSyncQueue = syncQueue; + mAppHeaderViewHolderFactory = appHeaderViewHolderFactory; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; + mGenericLinksParser = genericLinksParser; + mAssistContentRequester = assistContentRequester; + mMaximizeMenuFactory = maximizeMenuFactory; + mHandleMenuFactory = handleMenuFactory; + mMultiInstanceHelper = multiInstanceHelper; + mWindowManagerWrapper = windowManagerWrapper; + mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository; + } + + /** + * Register a listener to be called back when one of the tasks' maximize/restore action is + * triggered. + * TODO(b/346441962): hook this up to double-tap and the header's maximize button, instead of + * having the ViewModel deal with parsing motion events. + */ + void setOnMaximizeOrRestoreClickListener(Function0<Unit> listener) { + mOnMaximizeOrRestoreClickListener = listener; + } + + /** Registers a listener to be called when the decoration's snap-left action is triggered.*/ + void setOnLeftSnapClickListener(Function0<Unit> listener) { + mOnLeftSnapClickListener = listener; + } + + /** Registers a listener to be called when the decoration's snap-right action is triggered. */ + void setOnRightSnapClickListener(Function0<Unit> listener) { + mOnRightSnapClickListener = listener; + } + + /** Registers a listener to be called when the decoration's to-desktop action is triggered. */ + void setOnToDesktopClickListener(Consumer<DesktopModeTransitionSource> listener) { + mOnToDesktopClickListener = listener; + } + + /** + * Registers a listener to be called when the decoration's to-fullscreen action is triggered. + */ + void setOnToFullscreenClickListener(Function0<Unit> listener) { + mOnToFullscreenClickListener = listener; + } + + /** Registers a listener to be called when the decoration's to-split action is triggered. */ + void setOnToSplitScreenClickListener(Function0<Unit> listener) { + mOnToSplitscreenClickListener = listener; + } + + /** Registers a listener to be called when the decoration's new window action is triggered. */ + void setOnNewWindowClickListener(Function0<Unit> listener) { + mOnNewWindowClickListener = listener; + } + + /** + * Registers a listener to be called when the decoration's manage windows action is + * triggered. + */ + void setManageWindowsClickListener(Function0<Unit> listener) { + mOnManageWindowsClickListener = listener; } void setCaptionListeners( @@ -182,9 +334,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mDragPositioningCallback = dragPositioningCallback; } - void setDragDetector(DragDetector dragDetector) { - mDragDetector = dragDetector; - mDragDetector.setTouchSlop(ViewConfiguration.get(mContext).getScaledTouchSlop()); + void setOpenInBrowserClickListener(Consumer<Uri> listener) { + mOpenInBrowserClickListener = listener; } @Override @@ -200,8 +351,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // transaction (that applies task crop) is synced with the buffer transaction (that draws // the View). Both will be shown on screen at the same, whereas applying them independently // causes flickering. See b/270202228. - final boolean applyTransactionOnDraw = - taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM; + final boolean applyTransactionOnDraw = taskInfo.isFreeform(); relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskPositionAndCrop); if (!applyTransactionOnDraw) { t.apply(); @@ -212,74 +362,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { Trace.beginSection("DesktopModeWindowDecoration#relayout"); - if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { - // The Task is in Freeform mode -> show its header in sync since it's an integral part - // of the window itself - a delayed header might cause bad UX. - relayoutInSync(taskInfo, startT, finishT, applyStartTransactionOnDraw, - shouldSetTaskPositionAndCrop); - } else { - // The Task is outside Freeform mode -> allow the handle view to be delayed since the - // handle is just a small addition to the window. - relayoutWithDelayedViewHost(taskInfo, startT, finishT, applyStartTransactionOnDraw, - shouldSetTaskPositionAndCrop); - } - Trace.endSection(); - } - /** Run the whole relayout phase immediately without delay. */ - private void relayoutInSync(ActivityManager.RunningTaskInfo taskInfo, - SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, - boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { - // Clear the current ViewHost runnable as we will update the ViewHost here - clearCurrentViewHostRunnable(); - updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, applyStartTransactionOnDraw, - shouldSetTaskPositionAndCrop); - if (mResult.mRootView != null) { - updateViewHost(mRelayoutParams, startT, mResult); + if (Flags.enableDesktopWindowingAppToWeb()) { + setCapturedLink(taskInfo.capturedLink, taskInfo.capturedLinkTimestamp); } - } - /** - * Clear the current ViewHost runnable - to ensure it doesn't run once relayout params have been - * updated. - */ - private void clearCurrentViewHostRunnable() { - if (mCurrentViewHostRunnable != null) { - mHandler.removeCallbacks(mCurrentViewHostRunnable); - mCurrentViewHostRunnable = null; - } - } - - /** - * Relayout the window decoration but repost some of the work, to unblock the current callstack. - */ - private void relayoutWithDelayedViewHost(ActivityManager.RunningTaskInfo taskInfo, - SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, - boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { - if (applyStartTransactionOnDraw) { - throw new IllegalArgumentException( - "We cannot both sync viewhost ondraw and delay viewhost creation."); - } - // Clear the current ViewHost runnable as we will update the ViewHost here - clearCurrentViewHostRunnable(); - updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, - false /* applyStartTransactionOnDraw */, shouldSetTaskPositionAndCrop); - if (mResult.mRootView == null) { - // This means something blocks the window decor from showing, e.g. the task is hidden. - // Nothing is set up in this case including the decoration surface. - return; - } - // Store the current runnable so it can be removed if we start a new relayout. - mCurrentViewHostRunnable = mViewHostRunnable; - mHandler.post(mCurrentViewHostRunnable); - } - - private void updateRelayoutParamsAndSurfaces(ActivityManager.RunningTaskInfo taskInfo, - SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, - boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { - Trace.beginSection("DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces"); if (isHandleMenuActive()) { - mHandleMenu.relayout(startT); + mHandleMenu.relayout(startT, mResult.mCaptionX); } updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw, @@ -289,37 +378,96 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; final WindowContainerTransaction wct = new WindowContainerTransaction(); - Trace.beginSection("DesktopModeWindowDecoration#relayout-updateViewsAndSurfaces"); - updateViewsAndSurfaces(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); + Trace.beginSection("DesktopModeWindowDecoration#relayout-inner"); + relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); Trace.endSection(); // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo Trace.beginSection("DesktopModeWindowDecoration#relayout-applyWCT"); - mTaskOrganizer.applyTransaction(wct); + mBgExecutor.execute(() -> mTaskOrganizer.applyTransaction(wct)); Trace.endSection(); if (mResult.mRootView == null) { // This means something blocks the window decor from showing, e.g. the task is hidden. // Nothing is set up in this case including the decoration surface. - Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces + if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + notifyNoCaptionHandle(); + } + disposeStatusBarInputLayer(); + Trace.endSection(); // DesktopModeWindowDecoration#relayout return; } if (oldRootView != mResult.mRootView) { + disposeStatusBarInputLayer(); mWindowDecorViewHolder = createViewHolder(); } - Trace.beginSection("DesktopModeWindowDecoration#relayout-binding"); - mWindowDecorViewHolder.bindData(mTaskInfo); + + final Point position = new Point(); + if (isAppHandle(mWindowDecorViewHolder)) { + position.set(determineHandlePosition()); + } + Trace.beginSection("DesktopModeWindowDecoration#relayout-bindData"); + if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + notifyCaptionStateChanged(); + } + mWindowDecorViewHolder.bindData(mTaskInfo, + position, + mResult.mCaptionWidth, + mResult.mCaptionHeight, + isCaptionVisible()); Trace.endSection(); if (!mTaskInfo.isFocused) { closeHandleMenu(); + closeManageWindowsMenu(); closeMaximizeMenu(); } - updateDragResizeListener(oldDecorationSurface); updateMaximizeMenu(startT); - Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces + Trace.endSection(); // DesktopModeWindowDecoration#relayout + } + + private boolean isCaptionVisible() { + return mTaskInfo.isVisible && mIsCaptionVisible; + } + + private void setCapturedLink(Uri capturedLink, long timeStamp) { + if (capturedLink == null + || (mCapturedLink != null && mCapturedLink.mTimeStamp == timeStamp)) { + return; + } + mCapturedLink = new CapturedLink(capturedLink, timeStamp); + mHandler.postDelayed(mCapturedLinkExpiredRunnable, CAPTURED_LINK_TIMEOUT_MS); + } + + private void onCapturedLinkExpired() { + mHandler.removeCallbacks(mCapturedLinkExpiredRunnable); + if (mCapturedLink != null) { + mCapturedLink.setExpired(); + } + } + + @Nullable + private Uri getBrowserLink() { + // Do not show browser link in browser applications + final ComponentName baseActivity = mTaskInfo.baseActivity; + if (baseActivity != null && AppToWebUtils.isBrowserApp(mContext, + baseActivity.getPackageName(), mUserContext.getUserId())) { + return null; + } + // If the captured link is available and has not expired, return the captured link. + // Otherwise, return the generic link which is set to null if a generic link is unavailable. + if (mCapturedLink != null && !mCapturedLink.mExpired) { + return mCapturedLink.mUri; + } else if (mWebUri != null) { + return mWebUri; + } + return mGenericLink; + } + + UserHandle getUser() { + return mUserContext.getUser(); } private void updateDragResizeListener(SurfaceControl oldDecorationSurface) { @@ -350,14 +498,14 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()) .getScaledTouchSlop(); - mDragDetector.setTouchSlop(touchSlop); // If either task geometry or position have changed, update this task's // exclusion region listener final Resources res = mResult.mRootView.getResources(); if (mDragResizeListener.setGeometry( new DragResizeWindowGeometry(mRelayoutParams.mCornerRadius, - new Size(mResult.mWidth, mResult.mHeight), getResizeEdgeHandleSize(res), + new Size(mResult.mWidth, mResult.mHeight), + getResizeEdgeHandleSize(res), getResizeHandleEdgeInset(res), getFineResizeCornerSize(res), getLargeResizeCornerSize(res)), touchSlop) || !mTaskInfo.positionInParent.equals(mPositionInParent)) { updateExclusionRegion(); @@ -365,9 +513,67 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) { - final boolean isFreeform = - taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM; - return isFreeform && taskInfo.isResizeable; + if (DesktopModeFlags.ENABLE_WINDOWING_SCALED_RESIZING.isTrue()) { + return taskInfo.isFreeform(); + } + return taskInfo.isFreeform() && taskInfo.isResizeable; + } + + private void notifyCaptionStateChanged() { + // TODO: b/366159408 - Ensure bounds sent with notification account for RTL mode. + if (!canEnterDesktopMode(mContext) || !Flags.enableDesktopWindowingAppHandleEducation()) { + return; + } + if (!isCaptionVisible()) { + notifyNoCaptionHandle(); + } else if (isAppHandle(mWindowDecorViewHolder)) { + // App handle is visible since `mWindowDecorViewHolder` is of type + // [AppHandleViewHolder]. + final CaptionState captionState = new CaptionState.AppHandle(mTaskInfo, + isHandleMenuActive(), getCurrentAppHandleBounds()); + mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState); + } else { + // App header is visible since `mWindowDecorViewHolder` is of type + // [AppHeaderViewHolder]. + ((AppHeaderViewHolder) mWindowDecorViewHolder).runOnAppChipGlobalLayout( + () -> { + notifyAppChipStateChanged(); + return Unit.INSTANCE; + }); + } + } + + private void notifyNoCaptionHandle() { + if (!canEnterDesktopMode(mContext) || !Flags.enableDesktopWindowingAppHandleEducation()) { + return; + } + mWindowDecorCaptionHandleRepository.notifyCaptionChanged( + CaptionState.NoCaption.INSTANCE); + } + + private Rect getCurrentAppHandleBounds() { + return new Rect( + mResult.mCaptionX, + /* top= */0, + mResult.mCaptionX + mResult.mCaptionWidth, + mResult.mCaptionHeight); + } + + private void notifyAppChipStateChanged() { + final Rect appChipPositionInWindow = + ((AppHeaderViewHolder) mWindowDecorViewHolder).getAppChipLocationInWindow(); + final Rect taskBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); + final Rect appChipGlobalPosition = new Rect( + taskBounds.left + appChipPositionInWindow.left, + taskBounds.top + appChipPositionInWindow.top, + taskBounds.left + appChipPositionInWindow.right, + taskBounds.top + appChipPositionInWindow.bottom); + final CaptionState captionState = new CaptionState.AppHeader( + mTaskInfo, + isHandleMenuActive(), + appChipGlobalPosition); + + mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState); } private void updateMaximizeMenu(SurfaceControl.Transaction startT) { @@ -381,17 +587,45 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } } + private Point determineHandlePosition() { + final Point position = new Point(mResult.mCaptionX, 0); + if (mSplitScreenController.getSplitPosition(mTaskInfo.taskId) + == SPLIT_POSITION_BOTTOM_OR_RIGHT + && mDisplayController.getDisplayLayout(mTaskInfo.displayId).isLandscape() + ) { + // If this is the right split task, add left stage's width. + final Rect leftStageBounds = new Rect(); + mSplitScreenController.getStageBounds(leftStageBounds, new Rect()); + position.x += leftStageBounds.width(); + } + return position; + } + + /** + * Dispose of the view used to forward inputs in status bar region. Intended to be + * used any time handle is no longer visible. + */ + void disposeStatusBarInputLayer() { + if (!isAppHandle(mWindowDecorViewHolder) + || !Flags.enableHandleInputFix()) { + return; + } + asAppHandle(mWindowDecorViewHolder).disposeStatusBarInputLayer(); + } + private WindowDecorationViewHolder createViewHolder() { if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_handle) { return new AppHandleViewHolder( mResult.mRootView, mOnCaptionTouchListener, - mOnCaptionButtonClickListener + mOnCaptionButtonClickListener, + mWindowManagerWrapper, + mHandler ); } else if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_header) { loadAppInfoIfNeeded(); - return new AppHeaderViewHolder( + return mAppHeaderViewHolderFactory.create( mResult.mRootView, mOnCaptionTouchListener, mOnCaptionButtonClickListener, @@ -409,6 +643,26 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin throw new IllegalArgumentException("Unexpected layout resource id"); } + private boolean isAppHandle(WindowDecorationViewHolder viewHolder) { + return viewHolder instanceof AppHandleViewHolder; + } + + @Nullable + private AppHandleViewHolder asAppHandle(WindowDecorationViewHolder viewHolder) { + if (viewHolder instanceof AppHandleViewHolder) { + return (AppHandleViewHolder) viewHolder; + } + return null; + } + + @Nullable + private AppHeaderViewHolder asAppHeader(WindowDecorationViewHolder viewHolder) { + if (viewHolder instanceof AppHeaderViewHolder) { + return (AppHeaderViewHolder) viewHolder; + } + return null; + } + @VisibleForTesting static void updateRelayoutParams( RelayoutParams relayoutParams, @@ -425,6 +679,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin relayoutParams.mLayoutResId = captionLayoutId; relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode()); relayoutParams.mCaptionWidthId = getCaptionWidthId(relayoutParams.mLayoutResId); + // Allow the handle view to be delayed since the handle is just a small addition to the + // window, whereas the header cannot be delayed because it is expected to be visible from + // the first frame. + relayoutParams.mAsyncViewHost = isAppHandle; if (isAppHeader) { if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { @@ -432,6 +690,17 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // through to the windows below so that the app can respond to input events on // their custom content. relayoutParams.mInputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY; + } else { + if (ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION.isTrue()) { + // Force-consume the caption bar insets when the app tries to hide the caption. + // This improves app compatibility of immersive apps. + relayoutParams.mInsetSourceFlags |= FLAG_FORCE_CONSUMING; + } + } + if (ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS.isTrue()) { + // Always force-consume the caption bar insets for maximum app compatibility, + // including non-immersive apps that just don't handle caption insets properly. + relayoutParams.mInsetSourceFlags |= FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR; } // Report occluding elements as bounding rects to the insets system so that apps can // draw in the empty space in the center: @@ -445,11 +714,17 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final RelayoutParams.OccludingCaptionElement controlsElement = new RelayoutParams.OccludingCaptionElement(); controlsElement.mWidthResId = R.dimen.desktop_mode_customizable_caption_margin_end; + if (Flags.enableMinimizeButton()) { + controlsElement.mWidthResId = + R.dimen.desktop_mode_customizable_caption_with_minimize_button_margin_end; + } controlsElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.END; relayoutParams.mOccludingCaptionElements.add(controlsElement); - } else if (isAppHandle) { + } else if (isAppHandle && !Flags.enableHandleInputFix()) { // The focused decor (fullscreen/split) does not need to handle input because input in // the App Handle is handled by the InputMonitor in DesktopModeWindowDecorViewModel. + // Note: This does not apply with the above flag enabled as the status bar input layer + // will forward events to the handle directly. relayoutParams.mInputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; } @@ -468,7 +743,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // TODO(b/301119301): consider moving the config data needed for diffs to relayout params // instead of using a whole Configuration as a parameter. final Configuration windowDecorConfig = new Configuration(); - if (Flags.enableAppHeaderWithTaskDensity() && isAppHeader) { + if (DesktopModeFlags.ENABLE_APP_HEADER_WITH_TASK_DENSITY.isTrue() && isAppHeader) { // Should match the density of the task. The task may have had its density overridden // to be different that SysUI's. windowDecorConfig.setTo(taskInfo.configuration); @@ -556,17 +831,21 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (mAppIconBitmap != null && mAppName != null) { return; } - final ActivityInfo activityInfo = mTaskInfo.topActivityInfo; - if (activityInfo == null) { - Log.e(TAG, "Top activity info not found in task"); + final ComponentName baseActivity = mTaskInfo.baseActivity; + if (baseActivity == null) { + Slog.e(TAG, "Base activity component not found in task"); return; } - PackageManager pm = mContext.getApplicationContext().getPackageManager(); + final PackageManager pm = mUserContext.getPackageManager(); + final ActivityInfo activityInfo = pm.getActivityInfo(baseActivity, 0 /* flags */); final IconProvider provider = new IconProvider(mContext); final Drawable appIconDrawable = provider.getIcon(activityInfo); + final Drawable badgedAppIconDrawable = pm.getUserBadgedIcon(appIconDrawable, + UserHandle.of(mTaskInfo.userId)); final BaseIconFactory headerIconFactory = createIconFactory(mContext, R.dimen.desktop_mode_caption_icon_radius); - mAppIconBitmap = headerIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT); + mAppIconBitmap = headerIconFactory.createIconBitmap(badgedAppIconDrawable, + 1f /* scale */); final BaseIconFactory resizeVeilIconFactory = createIconFactory(mContext, R.dimen.desktop_mode_resize_veil_icon_size); @@ -575,6 +854,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final ApplicationInfo applicationInfo = activityInfo.applicationInfo; mAppName = pm.getApplicationLabel(applicationInfo); + } catch (PackageManager.NameNotFoundException e) { + Slog.e(TAG, "Base activity's component name cannot be found on the system", e); } finally { Trace.endSection(); } @@ -714,11 +995,45 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * Create and display maximize menu window */ void createMaximizeMenu() { - mMaximizeMenu = new MaximizeMenu(mSyncQueue, mRootTaskDisplayAreaOrganizer, - mDisplayController, mTaskInfo, mOnCaptionButtonClickListener, - mOnCaptionGenericMotionListener, mOnCaptionTouchListener, mContext, + mMaximizeMenu = mMaximizeMenuFactory.create(mSyncQueue, mRootTaskDisplayAreaOrganizer, + mDisplayController, mTaskInfo, mContext, calculateMaximizeMenuPosition(), mSurfaceControlTransactionSupplier); - mMaximizeMenu.show(); + mMaximizeMenu.show( + mOnMaximizeOrRestoreClickListener, + mOnLeftSnapClickListener, + mOnRightSnapClickListener, + hovered -> { + mIsMaximizeMenuHovered = hovered; + onMaximizeHoverStateChanged(); + return null; + }, + () -> { + closeMaximizeMenu(); + return null; + } + ); + } + + /** Set whether the app header's maximize button is hovered. */ + void setAppHeaderMaximizeButtonHovered(boolean hovered) { + mIsAppHeaderMaximizeButtonHovered = hovered; + onMaximizeHoverStateChanged(); + } + + /** + * Called when either one of the maximize button in the app header or the maximize menu has + * changed its hover state. + */ + void onMaximizeHoverStateChanged() { + if (!mIsMaximizeMenuHovered && !mIsAppHeaderMaximizeButtonHovered) { + // Neither is hovered, close the menu. + if (isMaximizeMenuActive()) { + mHandler.postDelayed(mCloseMaximizeWindowRunnable, CLOSE_MAXIMIZE_MENU_DELAY_MS); + } + return; + } + // At least one of the two is hovered, cancel the close if needed. + mHandler.removeCallbacks(mCloseMaximizeWindowRunnable); } /** @@ -726,7 +1041,15 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ void closeMaximizeMenu() { if (!isMaximizeMenuActive()) return; - mMaximizeMenu.close(); + mMaximizeMenu.close(() -> { + // Request the accessibility service to refocus on the maximize button after closing + // the menu. + final AppHeaderViewHolder appHeader = asAppHeader(mWindowDecorViewHolder); + if (appHeader != null) { + appHeader.requestAccessibilityFocus(); + } + return Unit.INSTANCE; + }); mMaximizeMenu = null; } @@ -735,23 +1058,127 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } /** - * Create and display handle menu window. + * Updates app info and creates and displays handle menu window. + */ + void createHandleMenu(boolean minimumInstancesFound) { + // Requests assist content. When content is received, calls {@link #onAssistContentReceived} + // which sets app info and creates the handle menu. + mMinimumInstancesFound = minimumInstancesFound; + mAssistContentRequester.requestAssistContent( + mTaskInfo.taskId, this::onAssistContentReceived); + } + + /** + * Called when assist content is received. updates the saved links and creates the handle menu. */ - void createHandleMenu(SplitScreenController splitScreenController) { + @VisibleForTesting + void onAssistContentReceived(@Nullable AssistContent assistContent) { + mWebUri = assistContent == null ? null : assistContent.getWebUri(); loadAppInfoIfNeeded(); - mHandleMenu = new HandleMenu.Builder(this) - .setAppIcon(mAppIconBitmap) - .setAppName(mAppName) - .setOnClickListener(mOnCaptionButtonClickListener) - .setOnTouchListener(mOnCaptionTouchListener) - .setLayoutId(mRelayoutParams.mLayoutResId) - .setWindowingButtonsVisible(DesktopModeStatus.canEnterDesktopMode(mContext)) - .setCaptionHeight(mResult.mCaptionHeight) - .setDisplayController(mDisplayController) - .setSplitScreenController(splitScreenController) - .build(); + updateGenericLink(); + final boolean supportsMultiInstance = mMultiInstanceHelper + .supportsMultiInstanceSplit(mTaskInfo.baseActivity) + && Flags.enableDesktopWindowingMultiInstanceFeatures(); + final boolean shouldShowManageWindowsButton = supportsMultiInstance + && mMinimumInstancesFound; + mHandleMenu = mHandleMenuFactory.create( + this, + mWindowManagerWrapper, + mRelayoutParams.mLayoutResId, + mAppIconBitmap, + mAppName, + mSplitScreenController, + canEnterDesktopMode(mContext), + supportsMultiInstance, + shouldShowManageWindowsButton, + getBrowserLink(), + mResult.mCaptionWidth, + mResult.mCaptionHeight, + mResult.mCaptionX + ); mWindowDecorViewHolder.onHandleMenuOpened(); - mHandleMenu.show(); + mHandleMenu.show( + /* onToDesktopClickListener= */ () -> { + mOnToDesktopClickListener.accept(APP_HANDLE_MENU_BUTTON); + mOnToDesktopClickListener.accept(APP_HANDLE_MENU_BUTTON); + return Unit.INSTANCE; + }, + /* onToFullscreenClickListener= */ mOnToFullscreenClickListener, + /* onToSplitScreenClickListener= */ mOnToSplitscreenClickListener, + /* onNewWindowClickListener= */ mOnNewWindowClickListener, + /* onManageWindowsClickListener= */ mOnManageWindowsClickListener, + /* openInBrowserClickListener= */ (uri) -> { + mOpenInBrowserClickListener.accept(uri); + onCapturedLinkExpired(); + return Unit.INSTANCE; + }, + /* onCloseMenuClickListener= */ () -> { + closeHandleMenu(); + return Unit.INSTANCE; + }, + /* onOutsideTouchListener= */ () -> { + closeHandleMenu(); + return Unit.INSTANCE; + } + ); + if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + notifyCaptionStateChanged(); + } + mMinimumInstancesFound = false; + } + + void createManageWindowsMenu(@NonNull List<Pair<Integer, TaskSnapshot>> snapshotList, + @NonNull Function1<Integer, Unit> onIconClickListener + ) { + if (mTaskInfo.isFreeform()) { + mManageWindowsMenu = new DesktopHeaderManageWindowsMenu( + mTaskInfo, + mDisplayController, + mRootTaskDisplayAreaOrganizer, + mContext, + mSurfaceControlBuilderSupplier, + mSurfaceControlTransactionSupplier, + snapshotList, + onIconClickListener, + /* onOutsideClickListener= */ () -> { + closeManageWindowsMenu(); + return Unit.INSTANCE; + } + ); + } else { + mManageWindowsMenu = new DesktopHandleManageWindowsMenu( + mTaskInfo, + mSplitScreenController, + getCaptionX(), + mResult.mCaptionWidth, + mWindowManagerWrapper, + mContext, + snapshotList, + onIconClickListener, + /* onOutsideClickListener= */ () -> { + closeManageWindowsMenu(); + return Unit.INSTANCE; + } + ); + } + } + + void closeManageWindowsMenu() { + if (mManageWindowsMenu != null) { + mManageWindowsMenu.close(); + } + mManageWindowsMenu = null; + } + + private void updateGenericLink() { + final ComponentName baseActivity = mTaskInfo.baseActivity; + if (baseActivity == null) { + return; + } + + final String genericLink = + mGenericLinksParser.getGenericLink(baseActivity.getPackageName()); + mGenericLink = genericLink == null ? null : Uri.parse(genericLink); } /** @@ -762,11 +1189,15 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mWindowDecorViewHolder.onHandleMenuClosed(); mHandleMenu.close(); mHandleMenu = null; + if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + notifyCaptionStateChanged(); + } } @Override void releaseViews(WindowContainerTransaction wct) { closeHandleMenu(); + closeManageWindowsMenu(); closeMaximizeMenu(); super.releaseViews(wct); } @@ -835,10 +1266,17 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * @return {@code true} if event is inside caption handle view, {@code false} if not */ boolean checkTouchEventInFocusedCaptionHandle(MotionEvent ev) { - if (isHandleMenuActive() || !(mWindowDecorViewHolder - instanceof AppHandleViewHolder)) { + if (isHandleMenuActive() || !isAppHandle(mWindowDecorViewHolder) + || Flags.enableHandleInputFix()) { return false; } + // The status bar input layer can only receive input in handle coordinates to begin with, + // so checking coordinates is unnecessary as input is always within handle bounds. + if (isAppHandle(mWindowDecorViewHolder) + && Flags.enableHandleInputFix() + && isCaptionVisible()) { + return true; + } return checkTouchEventInCaption(ev); } @@ -872,7 +1310,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * @param ev the MotionEvent to compare */ void checkTouchEvent(MotionEvent ev) { - if (mResult.mRootView == null) return; + if (mResult.mRootView == null || Flags.enableHandleInputFix()) return; final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); final View handle = caption.findViewById(R.id.caption_handle); final boolean inHandle = !isHandleMenuActive() @@ -882,8 +1320,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin handle.performClick(); } if (isHandleMenuActive()) { - mHandleMenu.checkMotionEvent(ev); - closeHandleMenuIfNeeded(ev); + // If the whole handle menu can be touched directly, rely on FLAG_WATCH_OUTSIDE_TOUCH. + // This is for the case that some of the handle menu is underneath the status bar. + if (isAppHandle(mWindowDecorViewHolder) + && !Flags.enableHandleInputFix()) { + mHandleMenu.checkMotionEvent(ev); + closeHandleMenuIfNeeded(ev); + } } } @@ -894,7 +1337,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * @param ev the MotionEvent to compare against. */ void updateHoverAndPressStatus(MotionEvent ev) { - if (mResult.mRootView == null) return; + if (mResult.mRootView == null || Flags.enableHandleInputFix()) return; final View handle = mResult.mRootView.findViewById(R.id.caption_handle); final boolean inHandle = !isHandleMenuActive() && checkTouchEventInFocusedCaptionHandle(ev); @@ -904,15 +1347,11 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // We want handle to remain pressed if the pointer moves outside of it during a drag. handle.setPressed((inHandle && action == ACTION_DOWN) || (handle.isPressed() && action != ACTION_UP && action != ACTION_CANCEL)); - if (isHandleMenuActive() && !isHandleMenuAboveStatusBar()) { + if (isHandleMenuActive()) { mHandleMenu.checkMotionEvent(ev); } } - private boolean isHandleMenuAboveStatusBar() { - return Flags.enableAdditionalWindowsAboveStatusBar() && !mTaskInfo.isFreeform(); - } - private boolean pointInView(View v, float x, float y) { return v != null && v.getLeft() <= x && v.getRight() >= x && v.getTop() <= y && v.getBottom() >= y; @@ -922,9 +1361,14 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin public void close() { closeDragResizeListener(); closeHandleMenu(); + closeManageWindowsMenu(); mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId); disposeResizeVeil(); - clearCurrentViewHostRunnable(); + disposeStatusBarInputLayer(); + if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + notifyNoCaptionHandle(); + } + super.close(); } @@ -951,7 +1395,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ private Region getGlobalExclusionRegion() { Region exclusionRegion; - if (mDragResizeListener != null && mTaskInfo.isResizeable) { + if (mDragResizeListener != null && isDragResizable(mTaskInfo)) { exclusionRegion = mDragResizeListener.getCornersRegion(); } else { exclusionRegion = new Region(); @@ -986,40 +1430,26 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return R.id.desktop_mode_caption; } - void setAnimatingTaskResize(boolean animatingTaskResize) { + void setAnimatingTaskResizeOrReposition(boolean animatingTaskResizeOrReposition) { if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_handle) return; - ((AppHeaderViewHolder) mWindowDecorViewHolder) - .setAnimatingTaskResize(animatingTaskResize); - } - - /** Called when there is a {@Link ACTION_HOVER_EXIT} on the maximize window button. */ - void onMaximizeWindowHoverExit() { - ((AppHeaderViewHolder) mWindowDecorViewHolder) - .onMaximizeWindowHoverExit(); + asAppHeader(mWindowDecorViewHolder) + .setAnimatingTaskResizeOrReposition(animatingTaskResizeOrReposition); } - /** Called when there is a {@Link ACTION_HOVER_ENTER} on the maximize window button. */ - void onMaximizeWindowHoverEnter() { - ((AppHeaderViewHolder) mWindowDecorViewHolder) - .onMaximizeWindowHoverEnter(); - } - - /** Called when there is a {@Link ACTION_HOVER_ENTER} on a view in the maximize menu. */ - void onMaximizeMenuHoverEnter(int id, MotionEvent ev) { - mMaximizeMenu.onMaximizeMenuHoverEnter(id, ev); - } - - /** Called when there is a {@Link ACTION_HOVER_MOVE} on a view in the maximize menu. */ - void onMaximizeMenuHoverMove(int id, MotionEvent ev) { - mMaximizeMenu.onMaximizeMenuHoverMove(id, ev); + /** + * Called when there is a {@link MotionEvent#ACTION_HOVER_EXIT} on the maximize window button. + */ + void onMaximizeButtonHoverExit() { + asAppHeader(mWindowDecorViewHolder).onMaximizeWindowHoverExit(); } - /** Called when there is a {@Link ACTION_HOVER_EXIT} on a view in the maximize menu. */ - void onMaximizeMenuHoverExit(int id, MotionEvent ev) { - mMaximizeMenu.onMaximizeMenuHoverExit(id, ev); + /** + * Called when there is a {@link MotionEvent#ACTION_HOVER_ENTER} on the maximize window button. + */ + void onMaximizeButtonHoverEnter() { + asAppHeader(mWindowDecorViewHolder).onMaximizeWindowHoverEnter(); } - @Override public String toString() { return "{" @@ -1034,24 +1464,59 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin DesktopModeWindowDecoration create( Context context, + @NonNull Context userContext, DisplayController displayController, + SplitScreenController splitScreenController, ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, Handler handler, + @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { + AppHeaderViewHolder.Factory appHeaderViewHolderFactory, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + AppToWebGenericLinksParser genericLinksParser, + AssistContentRequester assistContentRequester, + MultiInstanceHelper multiInstanceHelper, + WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, + WindowDecorViewHostSupplier windowDecorViewHostSupplier) { return new DesktopModeWindowDecoration( context, + userContext, displayController, + splitScreenController, taskOrganizer, taskInfo, taskSurface, handler, + bgExecutor, choreographer, syncQueue, - rootTaskDisplayAreaOrganizer); + appHeaderViewHolderFactory, + rootTaskDisplayAreaOrganizer, + genericLinksParser, + assistContentRequester, + multiInstanceHelper, + windowDecorCaptionHandleRepository, + windowDecorViewHostSupplier); + } + } + + @VisibleForTesting + static class CapturedLink { + private final long mTimeStamp; + private final Uri mUri; + private boolean mExpired; + + CapturedLink(@NonNull Uri uri, long timeStamp) { + mUri = uri; + mTimeStamp = timeStamp; + mExpired = false; + } + + void setExpired() { + mExpired = true; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java index da268988bac7..01bb7f74ba06 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java @@ -19,9 +19,14 @@ package com.android.wm.shell.windowdecor; import static android.view.InputDevice.SOURCE_TOUCHSCREEN; import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_HOVER_ENTER; +import static android.view.MotionEvent.ACTION_HOVER_EXIT; +import static android.view.MotionEvent.ACTION_HOVER_MOVE; import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_POINTER_UP; import static android.view.MotionEvent.ACTION_UP; +import android.annotation.NonNull; import android.graphics.PointF; import android.view.MotionEvent; import android.view.View; @@ -43,13 +48,19 @@ class DragDetector { private final PointF mInputDownPoint = new PointF(); private int mTouchSlop; private boolean mIsDragEvent; - private int mDragPointerId; + private int mDragPointerId = -1; + private final long mHoldToDragMinDurationMs; + private boolean mDidStrayBeforeFullHold; + private boolean mDidHoldForMinDuration; private boolean mResultOfDownAction; - DragDetector(MotionEventHandler eventHandler) { + DragDetector(@NonNull MotionEventHandler eventHandler, long holdToDragMinDurationMs, + int touchSlop) { resetState(); mEventHandler = eventHandler; + mHoldToDragMinDurationMs = holdToDragMinDurationMs; + mTouchSlop = touchSlop; } /** @@ -67,7 +78,7 @@ class DragDetector { * * @return the result returned by {@link #mEventHandler}, or the result when * {@link #mEventHandler} handles the previous down event if the event shouldn't be passed - */ + */ boolean onMotionEvent(View v, MotionEvent ev) { final boolean isTouchScreen = (ev.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN; @@ -86,35 +97,86 @@ class DragDetector { return mResultOfDownAction; } case ACTION_MOVE: { - if (ev.findPointerIndex(mDragPointerId) == -1) { - mDragPointerId = ev.getPointerId(0); + if (mDragPointerId == -1) { + // The primary pointer was lifted, ignore the rest of the gesture. + return mResultOfDownAction; } final int dragPointerIndex = ev.findPointerIndex(mDragPointerId); + if (dragPointerIndex == -1) { + throw new IllegalStateException("Failed to find primary pointer!"); + } if (!mIsDragEvent) { float dx = ev.getRawX(dragPointerIndex) - mInputDownPoint.x; float dy = ev.getRawY(dragPointerIndex) - mInputDownPoint.y; + final float dt = ev.getEventTime() - ev.getDownTime(); + final boolean pastTouchSlop = Math.hypot(dx, dy) > mTouchSlop; + final boolean withinHoldRegion = !pastTouchSlop; + + if (mHoldToDragMinDurationMs <= 0) { + mDidHoldForMinDuration = true; + } else { + if (!withinHoldRegion && dt < mHoldToDragMinDurationMs) { + // Mark as having strayed so that in case the (x,y) ends up in the + // original position we know it's not actually valid. + mDidStrayBeforeFullHold = true; + } + if (!mDidStrayBeforeFullHold && dt >= mHoldToDragMinDurationMs) { + mDidHoldForMinDuration = true; + } + } + // Touches generate noisy moves, so only once the move is past the touch // slop threshold should it be considered a drag. - mIsDragEvent = Math.hypot(dx, dy) > mTouchSlop; + mIsDragEvent = mDidHoldForMinDuration && pastTouchSlop; } // The event handler should only be notified about 'move' events if a drag has been // detected. - if (mIsDragEvent) { - return mEventHandler.handleMotionEvent(v, ev); - } else { + if (!mIsDragEvent) { + return mResultOfDownAction; + } + return mEventHandler.handleMotionEvent(v, + getSinglePointerEvent(ev, mDragPointerId)); + } + case ACTION_HOVER_ENTER: + case ACTION_HOVER_MOVE: + case ACTION_HOVER_EXIT: { + return mEventHandler.handleMotionEvent(v, + getSinglePointerEvent(ev, mDragPointerId)); + } + case ACTION_POINTER_UP: { + if (mDragPointerId == -1) { + // The primary pointer was lifted, ignore the rest of the gesture. return mResultOfDownAction; } + if (mDragPointerId != ev.getPointerId(ev.getActionIndex())) { + // Ignore a secondary pointer being lifted. + return mResultOfDownAction; + } + // The primary pointer is being lifted. + final int dragPointerId = mDragPointerId; + mDragPointerId = -1; + return mEventHandler.handleMotionEvent(v, getSinglePointerEvent(ev, dragPointerId)); } case ACTION_UP: case ACTION_CANCEL: { + final int dragPointerId = mDragPointerId; resetState(); - return mEventHandler.handleMotionEvent(v, ev); + if (dragPointerId == -1) { + // The primary pointer was lifted, ignore the rest of the gesture. + return mResultOfDownAction; + } + return mEventHandler.handleMotionEvent(v, getSinglePointerEvent(ev, dragPointerId)); } default: - return mEventHandler.handleMotionEvent(v, ev); + // Ignore other events. + return mResultOfDownAction; } } + private static MotionEvent getSinglePointerEvent(MotionEvent ev, int pointerId) { + return ev.getPointerCount() > 1 ? ev.split(1 << pointerId) : ev; + } + void setTouchSlop(int touchSlop) { mTouchSlop = touchSlop; } @@ -124,9 +186,11 @@ class DragDetector { mInputDownPoint.set(0, 0); mDragPointerId = -1; mResultOfDownAction = false; + mDidStrayBeforeFullHold = false; + mDidHoldForMinDuration = false; } interface MotionEventHandler { boolean handleMotionEvent(@Nullable View v, MotionEvent ev); } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java index fe1c9c3cce66..38f9cfaca7ae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java @@ -27,11 +27,13 @@ import android.graphics.PointF; import android.graphics.Rect; import android.util.DisplayMetrics; import android.view.SurfaceControl; +import android.window.flags.DesktopModeFlags; + +import androidx.annotation.NonNull; -import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.shared.DesktopModeStatus; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; /** * Utility class that contains logic common to classes implementing {@link DragPositioningCallback} @@ -80,50 +82,82 @@ public class DragPositioningCallbackUtility { final int oldRight = repositionTaskBounds.right; final int oldBottom = repositionTaskBounds.bottom; - repositionTaskBounds.set(taskBoundsAtDragStart); + boolean isAspectRatioMaintained = true; // Make sure the new resizing destination in any direction falls within the stable bounds. - // If not, set the bounds back to the old location that was valid to avoid conflicts with - // some regions such as the gesture area. if ((ctrlType & CTRL_TYPE_LEFT) != 0) { - final int candidateLeft = repositionTaskBounds.left + (int) delta.x; - repositionTaskBounds.left = (candidateLeft > stableBounds.left) - ? candidateLeft : oldLeft; + repositionTaskBounds.left = Math.max(repositionTaskBounds.left + (int) delta.x, + stableBounds.left); + if (repositionTaskBounds.left == stableBounds.left + && repositionTaskBounds.left + (int) delta.x != stableBounds.left) { + // If the task edge have been set to the stable bounds and not due to the users + // drag, the aspect ratio of the task will not be maintained. + isAspectRatioMaintained = false; + } } if ((ctrlType & CTRL_TYPE_RIGHT) != 0) { - final int candidateRight = repositionTaskBounds.right + (int) delta.x; - repositionTaskBounds.right = (candidateRight < stableBounds.right) - ? candidateRight : oldRight; + repositionTaskBounds.right = Math.min(repositionTaskBounds.right + (int) delta.x, + stableBounds.right); + if (repositionTaskBounds.right == stableBounds.right + && repositionTaskBounds.right + (int) delta.x != stableBounds.right) { + // If the task edge have been set to the stable bounds and not due to the users + // drag, the aspect ratio of the task will not be maintained. + isAspectRatioMaintained = false; + } } if ((ctrlType & CTRL_TYPE_TOP) != 0) { - final int candidateTop = repositionTaskBounds.top + (int) delta.y; - repositionTaskBounds.top = (candidateTop > stableBounds.top) - ? candidateTop : oldTop; + repositionTaskBounds.top = Math.max(repositionTaskBounds.top + (int) delta.y, + stableBounds.top); + if (repositionTaskBounds.top == stableBounds.top + && repositionTaskBounds.top + (int) delta.y != stableBounds.top) { + // If the task edge have been set to the stable bounds and not due to the users + // drag, the aspect ratio of the task will not be maintained. + isAspectRatioMaintained = false; + } } if ((ctrlType & CTRL_TYPE_BOTTOM) != 0) { - final int candidateBottom = repositionTaskBounds.bottom + (int) delta.y; - repositionTaskBounds.bottom = (candidateBottom < stableBounds.bottom) - ? candidateBottom : oldBottom; + repositionTaskBounds.bottom = Math.min(repositionTaskBounds.bottom + (int) delta.y, + stableBounds.bottom); + if (repositionTaskBounds.bottom == stableBounds.bottom + && repositionTaskBounds.bottom + (int) delta.y != stableBounds.bottom) { + // If the task edge have been set to the stable bounds and not due to the users + // drag, the aspect ratio of the task will not be maintained. + isAspectRatioMaintained = false; + } } - // If width or height are negative or less than the minimum width or height, revert the + + // If width or height are negative or exceeding the width or height constraints, revert the // respective bounds to use previous bound dimensions. - if (repositionTaskBounds.width() < getMinWidth(displayController, windowDecoration)) { + if (isExceedingWidthConstraint(repositionTaskBounds, stableBounds, displayController, + windowDecoration)) { repositionTaskBounds.right = oldRight; repositionTaskBounds.left = oldLeft; + isAspectRatioMaintained = false; } - if (repositionTaskBounds.height() < getMinHeight(displayController, windowDecoration)) { + if (isExceedingHeightConstraint(repositionTaskBounds, stableBounds, displayController, + windowDecoration)) { repositionTaskBounds.top = oldTop; repositionTaskBounds.bottom = oldBottom; + isAspectRatioMaintained = false; } - // If there are no changes to the bounds after checking new bounds against minimum width - // and height, do not set bounds and return false - if (oldLeft == repositionTaskBounds.left && oldTop == repositionTaskBounds.top - && oldRight == repositionTaskBounds.right - && oldBottom == repositionTaskBounds.bottom) { - return false; + + // If the application is unresizeable and any bounds have been set back to their old + // location or to a stable bound edge, reset all the bounds to maintain the applications + // aspect ratio. + if (DesktopModeFlags.ENABLE_WINDOWING_SCALED_RESIZING.isTrue() + && !isAspectRatioMaintained && !windowDecoration.mTaskInfo.isResizeable) { + repositionTaskBounds.top = oldTop; + repositionTaskBounds.bottom = oldBottom; + repositionTaskBounds.right = oldRight; + repositionTaskBounds.left = oldLeft; } - return true; + + // If there are no changes to the bounds after checking new bounds against minimum and + // maximum width and height, do not set bounds and return false + return oldLeft != repositionTaskBounds.left || oldTop != repositionTaskBounds.top + || oldRight != repositionTaskBounds.right + || oldBottom != repositionTaskBounds.bottom; } /** @@ -174,6 +208,30 @@ public class DragPositioningCallbackUtility { return result; } + private static boolean isExceedingWidthConstraint(@NonNull Rect repositionTaskBounds, + Rect maxResizeBounds, DisplayController displayController, + WindowDecoration windowDecoration) { + // Check if width is less than the minimum width constraint. + if (repositionTaskBounds.width() < getMinWidth(displayController, windowDecoration)) { + return true; + } + // Check if width is more than the maximum resize bounds on desktop windowing mode. + return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext) + && repositionTaskBounds.width() > maxResizeBounds.width(); + } + + private static boolean isExceedingHeightConstraint(@NonNull Rect repositionTaskBounds, + Rect maxResizeBounds, DisplayController displayController, + WindowDecoration windowDecoration) { + // Check if height is less than the minimum height constraint. + if (repositionTaskBounds.height() < getMinHeight(displayController, windowDecoration)) { + return true; + } + // Check if height is more than the maximum resize bounds on desktop windowing mode. + return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext) + && repositionTaskBounds.height() > maxResizeBounds.height(); + } + private static float getMinWidth(DisplayController displayController, WindowDecoration windowDecoration) { return windowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinWidth(displayController, @@ -210,14 +268,14 @@ public class DragPositioningCallbackUtility { private static float getDefaultMinSize(DisplayController displayController, WindowDecoration windowDecoration) { - float density = displayController.getDisplayLayout(windowDecoration.mTaskInfo.displayId) + float density = displayController.getDisplayLayout(windowDecoration.mTaskInfo.displayId) .densityDpi() * DisplayMetrics.DENSITY_DEFAULT_SCALE; return windowDecoration.mTaskInfo.defaultMinSize * density; } private static boolean isSizeConstraintForDesktopModeEnabled(Context context) { return DesktopModeStatus.canEnterDesktopMode(context) - && Flags.enableDesktopWindowingSizeConstraints(); + && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS.isTrue(); } interface DragStartListener { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java index d902444d4b15..4ff394e2b1a9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -17,6 +17,7 @@ package com.android.wm.shell.windowdecor; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; @@ -55,7 +56,7 @@ import android.view.ViewConfiguration; import android.view.WindowManagerGlobal; import android.window.InputTransferToken; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; @@ -81,6 +82,7 @@ class DragResizeInputListener implements AutoCloseable { private final InputChannel mInputChannel; private final TaskResizeInputEventReceiver mInputEventReceiver; + private final Context mContext; private final SurfaceControl mInputSinkSurface; private final IBinder mSinkClientToken; private final InputChannel mSinkInputChannel; @@ -97,6 +99,7 @@ class DragResizeInputListener implements AutoCloseable { Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, DisplayController displayController) { + mContext = context; mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier; mDisplayId = displayId; mDecorationSurface = decorationSurface; @@ -110,7 +113,7 @@ class DragResizeInputListener implements AutoCloseable { mDecorationSurface, mClientToken, null /* hostInputToken */, - FLAG_NOT_FOCUSABLE, + FLAG_NOT_FOCUSABLE | FLAG_SPLIT_TOUCH, PRIVATE_FLAG_TRUSTED_OVERLAY, INPUT_FEATURE_SPY, TYPE_APPLICATION, @@ -315,7 +318,8 @@ class DragResizeInputListener implements AutoCloseable { } }; - mDragDetector = new DragDetector(this); + mDragDetector = new DragDetector(this, 0 /* holdToDragMinDurationMs */, + ViewConfiguration.get(mContext).getScaledTouchSlop()); mDisplayLayoutSizeSupplier = displayLayoutSizeSupplier; mTouchRegionConsumer = touchRegionConsumer; } @@ -395,7 +399,7 @@ class DragResizeInputListener implements AutoCloseable { // Touch events are tracked in four corners. Other events are tracked in resize edges. switch (e.getActionMasked()) { case MotionEvent.ACTION_DOWN: { - mShouldHandleEvents = mDragResizeWindowGeometry.shouldHandleEvent(e, + mShouldHandleEvents = mDragResizeWindowGeometry.shouldHandleEvent(mContext, e, new Point() /* offset */); if (mShouldHandleEvents) { // Save the id of the pointer for this drag interaction; we will use the @@ -406,7 +410,8 @@ class DragResizeInputListener implements AutoCloseable { float rawX = e.getRawX(0); float rawY = e.getRawY(0); final int ctrlType = mDragResizeWindowGeometry.calculateCtrlType( - isEventFromTouchscreen(e), isEdgeResizePermitted(e), x, y); + isEventFromTouchscreen(e), isEdgeResizePermitted(e), x, + y); ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: Handling action down, update ctrlType to %d", TAG, ctrlType); mDragStartTaskBounds = mCallback.onDragPositioningStart(ctrlType, @@ -468,8 +473,7 @@ class DragResizeInputListener implements AutoCloseable { case MotionEvent.ACTION_HOVER_ENTER: case MotionEvent.ACTION_HOVER_MOVE: { updateCursorType(e.getDisplayId(), e.getDeviceId(), - e.getPointerId(/*pointerIndex=*/0), e.getXCursorPosition(), - e.getYCursorPosition()); + e.getPointerId(/*pointerIndex=*/0), e.getX(), e.getY()); result = true; break; } @@ -497,8 +501,8 @@ class DragResizeInputListener implements AutoCloseable { // Since we are handling cursor, we know that this is not a touchscreen event, and // that edge resizing should always be allowed. @DragPositioningCallback.CtrlType int ctrlType = - mDragResizeWindowGeometry.calculateCtrlType(/* isTouchscreen= */ - false, /* isEdgeResizePermitted= */ true, x, y); + mDragResizeWindowGeometry.calculateCtrlType(/* isTouchscreen= */ false, + /* isEdgeResizePermitted= */ true, x, y); int cursorType = PointerIcon.TYPE_DEFAULT; switch (ctrlType) { @@ -538,7 +542,7 @@ class DragResizeInputListener implements AutoCloseable { } private boolean shouldHandleEvent(MotionEvent e, Point offset) { - return mDragResizeWindowGeometry.shouldHandleEvent(e, offset); + return mDragResizeWindowGeometry.shouldHandleEvent(mContext, e, offset); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java index b5d1d4a76342..d726f5083eb6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java @@ -16,9 +16,10 @@ package com.android.wm.shell.windowdecor; +import static android.view.InputDevice.SOURCE_MOUSE; import static android.view.InputDevice.SOURCE_TOUCHSCREEN; +import static android.window.flags.DesktopModeFlags.ENABLE_WINDOWING_EDGE_DRAG_RESIZE; -import static com.android.window.flags.Flags.enableWindowingEdgeDragResize; import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM; import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT; import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT; @@ -26,6 +27,7 @@ import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED; import android.annotation.NonNull; +import android.content.Context; import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; @@ -33,9 +35,6 @@ import android.graphics.Region; import android.util.Size; import android.view.MotionEvent; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - import com.android.wm.shell.R; import java.util.Objects; @@ -44,53 +43,53 @@ import java.util.Objects; * Geometry for a drag resize region for a particular window. */ final class DragResizeWindowGeometry { - // TODO(b/337264971) clean up when no longer needed - @VisibleForTesting static final boolean DEBUG = true; - // The additional width to apply to edge resize bounds just for logging when a touch is - // close. - @VisibleForTesting static final int EDGE_DEBUG_BUFFER = 15; private final int mTaskCornerRadius; private final Size mTaskSize; - // The size of the handle applied to the edges of the window, for the user to drag resize. - private final int mResizeHandleThickness; + // The size of the handle outside the task window applied to the edges of the window, for the + // user to drag resize. + private final int mResizeHandleEdgeOutset; + // The size of the handle inside the task window applied to the edges of the window, for the + // user to drag resize. + private final int mResizeHandleEdgeInset; // The task corners to permit drag resizing with a course input, such as touch. - private final @NonNull TaskCorners mLargeTaskCorners; // The task corners to permit drag resizing with a fine input, such as stylus or cursor. private final @NonNull TaskCorners mFineTaskCorners; // The bounds for each edge drag region, which can resize the task in one direction. - private final @NonNull TaskEdges mTaskEdges; - // Extra-large edge bounds for logging to help debug when an edge resize is ignored. - private final @Nullable TaskEdges mDebugTaskEdges; + final @NonNull TaskEdges mTaskEdges; DragResizeWindowGeometry(int taskCornerRadius, @NonNull Size taskSize, - int resizeHandleThickness, int fineCornerSize, int largeCornerSize) { + int resizeHandleEdgeOutset, int resizeHandleEdgeInset, int fineCornerSize, + int largeCornerSize) { mTaskCornerRadius = taskCornerRadius; mTaskSize = taskSize; - mResizeHandleThickness = resizeHandleThickness; + mResizeHandleEdgeOutset = resizeHandleEdgeOutset; + mResizeHandleEdgeInset = resizeHandleEdgeInset; mLargeTaskCorners = new TaskCorners(mTaskSize, largeCornerSize); mFineTaskCorners = new TaskCorners(mTaskSize, fineCornerSize); // Save touch areas for each edge. - mTaskEdges = new TaskEdges(mTaskSize, mResizeHandleThickness); - if (DEBUG) { - mDebugTaskEdges = new TaskEdges(mTaskSize, mResizeHandleThickness + EDGE_DEBUG_BUFFER); - } else { - mDebugTaskEdges = null; - } + mTaskEdges = new TaskEdges(mTaskSize, mResizeHandleEdgeOutset, mResizeHandleEdgeInset); } /** * Returns the resource value to use for the resize handle on the edge of the window. */ static int getResizeEdgeHandleSize(@NonNull Resources res) { - return enableWindowingEdgeDragResize() - ? res.getDimensionPixelSize(R.dimen.desktop_mode_edge_handle) + return ENABLE_WINDOWING_EDGE_DRAG_RESIZE.isTrue() + ? res.getDimensionPixelSize(R.dimen.freeform_edge_handle_outset) : res.getDimensionPixelSize(R.dimen.freeform_resize_handle); } /** + * Returns the resource value to use for the edge resize handle inside the task bounds. + */ + static int getResizeHandleEdgeInset(@NonNull Resources res) { + return res.getDimensionPixelSize(R.dimen.freeform_edge_handle_inset); + } + + /** * Returns the resource value to use for course input, such as touch, that benefits from a large * square on each of the window's corners. */ @@ -109,7 +108,8 @@ final class DragResizeWindowGeometry { /** * Returns the size of the task this geometry is calculated for. */ - @NonNull Size getTaskSize() { + @NonNull + Size getTaskSize() { // Safe to return directly since size is immutable. return mTaskSize; } @@ -120,15 +120,9 @@ final class DragResizeWindowGeometry { */ void union(@NonNull Region region) { // Apply the edge resize regions. - if (inDebugMode()) { - // Use the larger edge sizes if we are debugging, to be able to log if we ignored a - // touch due to the size of the edge region. - mDebugTaskEdges.union(region); - } else { - mTaskEdges.union(region); - } + mTaskEdges.union(region); - if (enableWindowingEdgeDragResize()) { + if (ENABLE_WINDOWING_EDGE_DRAG_RESIZE.isTrue()) { // Apply the corners as well for the larger corners, to ensure we capture all possible // touches. mLargeTaskCorners.union(region); @@ -141,11 +135,12 @@ final class DragResizeWindowGeometry { /** * Returns if this MotionEvent should be handled, based on its source and position. */ - boolean shouldHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) { + boolean shouldHandleEvent(@NonNull Context context, @NonNull MotionEvent e, + @NonNull Point offset) { final float x = e.getX(0) + offset.x; final float y = e.getY(0) + offset.y; - if (enableWindowingEdgeDragResize()) { + if (ENABLE_WINDOWING_EDGE_DRAG_RESIZE.isTrue()) { // First check if touch falls within a corner. // Large corner bounds are used for course input like touch, otherwise fine bounds. boolean result = isEventFromTouchscreen(e) @@ -170,9 +165,12 @@ final class DragResizeWindowGeometry { } static boolean isEdgeResizePermitted(@NonNull MotionEvent e) { - if (enableWindowingEdgeDragResize()) { + if (ENABLE_WINDOWING_EDGE_DRAG_RESIZE.isTrue()) { return e.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS - || e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE; + || e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE + // Touchpad input + || (e.isFromSource(SOURCE_MOUSE) + && e.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER); } else { return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE; } @@ -196,7 +194,7 @@ final class DragResizeWindowGeometry { */ @DragPositioningCallback.CtrlType int calculateCtrlType(boolean isTouchscreen, boolean isEdgeResizePermitted, float x, float y) { - if (enableWindowingEdgeDragResize()) { + if (ENABLE_WINDOWING_EDGE_DRAG_RESIZE.isTrue()) { // First check if touch falls within a corner. // Large corner bounds are used for course input like touch, otherwise fine bounds. int ctrlType = isTouchscreen @@ -219,10 +217,6 @@ final class DragResizeWindowGeometry { @DragPositioningCallback.CtrlType private int calculateEdgeResizeCtrlType(float x, float y) { - if (inDebugMode() && (mDebugTaskEdges.contains((int) x, (int) y) - && !mTaskEdges.contains((int) x, (int) y))) { - return CTRL_TYPE_UNDEFINED; - } int ctrlType = CTRL_TYPE_UNDEFINED; // mTaskCornerRadius is only used in comparing with corner regions. Comparisons with // sides will use the bounds specified in setGeometry and not go into task bounds. @@ -239,13 +233,15 @@ final class DragResizeWindowGeometry { ctrlType |= CTRL_TYPE_BOTTOM; } // If the touch is within one of the four corners, check if it is within the bounds of the - // // handle. + // handle. if ((ctrlType & (CTRL_TYPE_LEFT | CTRL_TYPE_RIGHT)) != 0 && (ctrlType & (CTRL_TYPE_TOP | CTRL_TYPE_BOTTOM)) != 0) { return checkDistanceFromCenter(ctrlType, x, y); } - // Otherwise, we should make sure we don't resize tasks inside task bounds. - return (x < 0 || y < 0 || x >= mTaskSize.getWidth() || y >= mTaskSize.getHeight()) + // Allow a small resize handle inside the task bounds defined by the edge inset. + return (x <= mResizeHandleEdgeInset || y <= mResizeHandleEdgeInset + || x >= mTaskSize.getWidth() - mResizeHandleEdgeInset + || y >= mTaskSize.getHeight() - mResizeHandleEdgeInset) ? ctrlType : CTRL_TYPE_UNDEFINED; } @@ -259,7 +255,7 @@ final class DragResizeWindowGeometry { final Point cornerRadiusCenter = calculateCenterForCornerRadius(ctrlType); double distanceFromCenter = Math.hypot(x - cornerRadiusCenter.x, y - cornerRadiusCenter.y); - if (distanceFromCenter < mTaskCornerRadius + mResizeHandleThickness + if (distanceFromCenter < mTaskCornerRadius + mResizeHandleEdgeOutset && distanceFromCenter >= mTaskCornerRadius) { return ctrlType; } @@ -310,12 +306,11 @@ final class DragResizeWindowGeometry { return this.mTaskCornerRadius == other.mTaskCornerRadius && this.mTaskSize.equals(other.mTaskSize) - && this.mResizeHandleThickness == other.mResizeHandleThickness + && this.mResizeHandleEdgeOutset == other.mResizeHandleEdgeOutset + && this.mResizeHandleEdgeInset == other.mResizeHandleEdgeInset && this.mFineTaskCorners.equals(other.mFineTaskCorners) && this.mLargeTaskCorners.equals(other.mLargeTaskCorners) - && (inDebugMode() - ? this.mDebugTaskEdges.equals(other.mDebugTaskEdges) - : this.mTaskEdges.equals(other.mTaskEdges)); + && this.mTaskEdges.equals(other.mTaskEdges); } @Override @@ -323,14 +318,11 @@ final class DragResizeWindowGeometry { return Objects.hash( mTaskCornerRadius, mTaskSize, - mResizeHandleThickness, + mResizeHandleEdgeOutset, + mResizeHandleEdgeInset, mFineTaskCorners, mLargeTaskCorners, - (inDebugMode() ? mDebugTaskEdges : mTaskEdges)); - } - - private boolean inDebugMode() { - return DEBUG && mDebugTaskEdges != null; + mTaskEdges); } /** @@ -449,26 +441,27 @@ final class DragResizeWindowGeometry { private final @NonNull Rect mBottomEdgeBounds; private final @NonNull Region mRegion; - private TaskEdges(@NonNull Size taskSize, int resizeHandleThickness) { + private TaskEdges(@NonNull Size taskSize, int resizeHandleThickness, + int resizeHandleEdgeInset) { // Save touch areas for each edge. mTopEdgeBounds = new Rect( -resizeHandleThickness, -resizeHandleThickness, taskSize.getWidth() + resizeHandleThickness, - 0); + resizeHandleThickness); mLeftEdgeBounds = new Rect( -resizeHandleThickness, 0, - 0, + resizeHandleEdgeInset, taskSize.getHeight()); mRightEdgeBounds = new Rect( - taskSize.getWidth(), + taskSize.getWidth() - resizeHandleEdgeInset, 0, taskSize.getWidth() + resizeHandleThickness, taskSize.getHeight()); mBottomEdgeBounds = new Rect( -resizeHandleThickness, - taskSize.getHeight(), + taskSize.getHeight() - resizeHandleEdgeInset, taskSize.getWidth() + resizeHandleThickness, taskSize.getHeight() + resizeHandleThickness); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt new file mode 100644 index 000000000000..3885761d0742 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2024 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.windowdecor + +import android.app.ActivityManager.RunningTaskInfo +import android.graphics.PointF +import android.graphics.Rect +import com.android.internal.annotations.VisibleForTesting +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED +import com.android.wm.shell.windowdecor.DragPositioningCallback.CtrlType +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +/** + * [AbstractTaskPositionerDecorator] implementation for validating the coordinates associated with a + * drag action, to maintain a fixed aspect ratio before being used by the task positioner. + */ +class FixedAspectRatioTaskPositionerDecorator ( + private val windowDecoration: DesktopModeWindowDecoration, + decoratedTaskPositioner: TaskPositioner +) : AbstractTaskPositionerDecorator(decoratedTaskPositioner) { + + private var originalCtrlType = CTRL_TYPE_UNDEFINED + private var edgeResizeCtrlType = CTRL_TYPE_UNDEFINED + private val lastRepositionedBounds = Rect() + private val startingPoint = PointF() + private val lastValidPoint = PointF() + private var startingAspectRatio = 0f + private var isTaskPortrait = false + + override fun onDragPositioningStart(@CtrlType ctrlType: Int, x: Float, y: Float): Rect { + originalCtrlType = ctrlType + if (!requiresFixedAspectRatio()) { + return super.onDragPositioningStart(originalCtrlType, x, y) + } + + lastRepositionedBounds.set(getBounds(windowDecoration.mTaskInfo)) + startingPoint.set(x, y) + lastValidPoint.set(x, y) + val startingBoundWidth = lastRepositionedBounds.width() + val startingBoundHeight = lastRepositionedBounds.height() + startingAspectRatio = max(startingBoundWidth, startingBoundHeight).toFloat() / + min(startingBoundWidth, startingBoundHeight).toFloat() + isTaskPortrait = startingBoundWidth <= startingBoundHeight + + lastRepositionedBounds.set( + when (originalCtrlType) { + // If resize in an edge resize, adjust ctrlType passed to onDragPositioningStart() to + // mimic a corner resize instead. As at lest two adjacent edges need to be resized + // in relation to each other to maintain the apps aspect ratio. The additional adjacent + // edge is selected based on its proximity (closest) to the start of the drag. + CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT -> { + val verticalMidPoint = lastRepositionedBounds.top + (startingBoundHeight / 2) + edgeResizeCtrlType = originalCtrlType + + if (y < verticalMidPoint) CTRL_TYPE_TOP else CTRL_TYPE_BOTTOM + super.onDragPositioningStart(edgeResizeCtrlType, x, y) + } + CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM -> { + val horizontalMidPoint = lastRepositionedBounds.left + (startingBoundWidth / 2) + edgeResizeCtrlType = originalCtrlType + + if (x < horizontalMidPoint) CTRL_TYPE_LEFT else CTRL_TYPE_RIGHT + super.onDragPositioningStart(edgeResizeCtrlType, x, y) + } + // If resize is corner resize, no alteration to the ctrlType needs to be made. + else -> { + edgeResizeCtrlType = CTRL_TYPE_UNDEFINED + super.onDragPositioningStart(originalCtrlType, x, y) + } + } + ) + return lastRepositionedBounds + } + + override fun onDragPositioningMove(x: Float, y: Float): Rect { + if (!requiresFixedAspectRatio()) { + return super.onDragPositioningMove(x, y) + } + + val diffX = x - lastValidPoint.x + val diffY = y - lastValidPoint.y + when (originalCtrlType) { + CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT, CTRL_TYPE_TOP + CTRL_TYPE_LEFT -> { + if ((diffX > 0 && diffY > 0) || (diffX < 0 && diffY < 0)) { + // Drag coordinate falls within valid region (90 - 180 degrees or 270- 360 + // degrees from the corner the previous valid point). Allow resize with adjusted + // coordinates to maintain aspect ratio. + lastRepositionedBounds.set(dragAdjustedMove(x, y)) + } + } + CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT, CTRL_TYPE_TOP + CTRL_TYPE_RIGHT -> { + if ((diffX > 0 && diffY < 0) || (diffX < 0 && diffY > 0)) { + // Drag coordinate falls within valid region (180 - 270 degrees or 0 - 90 + // degrees from the corner the previous valid point). Allow resize with adjusted + // coordinates to maintain aspect ratio. + lastRepositionedBounds.set(dragAdjustedMove(x, y)) + } + } + CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT -> { + // If resize is on left or right edge, always adjust the y coordinate. + val adjustedY = getScaledChangeForY(x) + lastValidPoint.set(x, adjustedY) + lastRepositionedBounds.set(super.onDragPositioningMove(x, adjustedY)) + } + CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM -> { + // If resize is on top or bottom edge, always adjust the x coordinate. + val adjustedX = getScaledChangeForX(y) + lastValidPoint.set(adjustedX, y) + lastRepositionedBounds.set(super.onDragPositioningMove(adjustedX, y)) + } + } + return lastRepositionedBounds + } + + override fun onDragPositioningEnd(x: Float, y: Float): Rect { + if (!requiresFixedAspectRatio()) { + return super.onDragPositioningEnd(x, y) + } + + val diffX = x - lastValidPoint.x + val diffY = y - lastValidPoint.y + + when (originalCtrlType) { + CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT, CTRL_TYPE_TOP + CTRL_TYPE_LEFT -> { + if ((diffX > 0 && diffY > 0) || (diffX < 0 && diffY < 0)) { + // Drag coordinate falls within valid region (90 - 180 degrees or 270- 360 + // degrees from the corner the previous valid point). End resize with adjusted + // coordinates to maintain aspect ratio. + return dragAdjustedEnd(x, y) + } + // If end of resize is not within valid region, end resize from last valid + // coordinates. + return super.onDragPositioningEnd(lastValidPoint.x, lastValidPoint.y) + } + CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT, CTRL_TYPE_TOP + CTRL_TYPE_RIGHT -> { + if ((diffX > 0 && diffY < 0) || (diffX < 0 && diffY > 0)) { + // Drag coordinate falls within valid region (180 - 260 degrees or 0 - 90 + // degrees from the corner the previous valid point). End resize with adjusted + // coordinates to maintain aspect ratio. + return dragAdjustedEnd(x, y) + } + // If end of resize is not within valid region, end resize from last valid + // coordinates. + return super.onDragPositioningEnd(lastValidPoint.x, lastValidPoint.y) + } + CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT -> { + // If resize is on left or right edge, always adjust the y coordinate. + return super.onDragPositioningEnd(x, getScaledChangeForY(x)) + } + CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM -> { + // If resize is on top or bottom edge, always adjust the x coordinate. + return super.onDragPositioningEnd(getScaledChangeForX(y), y) + } + else -> { + return super.onDragPositioningEnd(x, y) + } + } + } + + private fun dragAdjustedMove(x: Float, y: Float): Rect { + val absDiffX = abs(x - lastValidPoint.x) + val absDiffY = abs(y - lastValidPoint.y) + if (absDiffY < absDiffX) { + lastValidPoint.set(getScaledChangeForX(y), y) + return super.onDragPositioningMove(getScaledChangeForX(y), y) + } + lastValidPoint.set(x, getScaledChangeForY(x)) + return super.onDragPositioningMove(x, getScaledChangeForY(x)) + } + + private fun dragAdjustedEnd(x: Float, y: Float): Rect { + val absDiffX = abs(x - lastValidPoint.x) + val absDiffY = abs(y - lastValidPoint.y) + if (absDiffY < absDiffX) { + return super.onDragPositioningEnd(getScaledChangeForX(y), y) + } + return super.onDragPositioningEnd(x, getScaledChangeForY(x)) + } + + /** + * Calculate the required change in the y dimension, given the change in the x dimension, to + * maintain the applications starting aspect ratio when resizing to a given x coordinate. + */ + private fun getScaledChangeForY(x: Float): Float { + val changeXDimension = x - startingPoint.x + val changeYDimension = if (isTaskPortrait) { + changeXDimension * startingAspectRatio + } else { + changeXDimension / startingAspectRatio + } + if (originalCtrlType.isBottomRightOrTopLeftCorner() + || edgeResizeCtrlType.isBottomRightOrTopLeftCorner()) { + return startingPoint.y + changeYDimension + } + return startingPoint.y - changeYDimension + } + + /** + * Calculate the required change in the x dimension, given the change in the y dimension, to + * maintain the applications starting aspect ratio when resizing to a given y coordinate. + */ + private fun getScaledChangeForX(y: Float): Float { + val changeYDimension = y - startingPoint.y + val changeXDimension = if (isTaskPortrait) { + changeYDimension / startingAspectRatio + } else { + changeYDimension * startingAspectRatio + } + if (originalCtrlType.isBottomRightOrTopLeftCorner() + || edgeResizeCtrlType.isBottomRightOrTopLeftCorner()) { + return startingPoint.x + changeXDimension + } + return startingPoint.x - changeXDimension + } + + /** + * If the action being triggered originated from the bottom right or top left corner of the + * window. + */ + private fun @receiver:CtrlType Int.isBottomRightOrTopLeftCorner(): Boolean { + return this == CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT || this == CTRL_TYPE_TOP + CTRL_TYPE_LEFT + } + + /** + * If the action being triggered is a resize action. + */ + private fun @receiver:CtrlType Int.isResizing(): Boolean { + return (this and CTRL_TYPE_TOP) != 0 || (this and CTRL_TYPE_BOTTOM) != 0 + || (this and CTRL_TYPE_LEFT) != 0 || (this and CTRL_TYPE_RIGHT) != 0 + } + + /** + * Whether the aspect ratio of the activity needs to be maintained during the current drag + * action. If the current action is not a resize (there is no bounds change) so the aspect ratio + * is already maintained and does not need handling here. If the activity is resizeable, it + * can handle aspect ratio changes itself so again we do not need to handle it here. + */ + private fun requiresFixedAspectRatio(): Boolean { + return originalCtrlType.isResizing() && !windowDecoration.mTaskInfo.isResizeable + } + + @VisibleForTesting + fun getBounds(taskInfo: RunningTaskInfo): Rect { + return taskInfo.configuration.windowConfiguration.bounds + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java index 76096b0c59f3..3853f1f086c1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java @@ -49,8 +49,7 @@ import java.util.function.Supplier; * that we send the final shell transition since we still utilize the {@link #onTransitionConsumed} * callback. */ -class FluidResizeTaskPositioner implements DragPositioningCallback, - TaskDragResizer, Transitions.TransitionHandler { +class FluidResizeTaskPositioner implements TaskPositioner, Transitions.TransitionHandler { private final ShellTaskOrganizer mTaskOrganizer; private final Transitions mTransitions; private final WindowDecoration mWindowDecoration; @@ -152,11 +151,8 @@ class FluidResizeTaskPositioner implements DragPositioningCallback, } mDragResizeEndTransition = mTransitions.startTransition(TRANSIT_CHANGE, wct, this); } else if (mCtrlType == CTRL_TYPE_UNDEFINED) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds, mTaskBoundsAtDragStart, mRepositionStartPoint, x, y); - wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds); - mTransitions.startTransition(TRANSIT_CHANGE, wct, this); } mTaskBoundsAtDragStart.setEmpty(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java deleted file mode 100644 index df0836c1121d..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java +++ /dev/null @@ -1,516 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.windowdecor; - -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; -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.MotionEvent.ACTION_DOWN; -import static android.view.MotionEvent.ACTION_UP; - -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.ActivityManager.RunningTaskInfo; -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Point; -import android.graphics.PointF; -import android.graphics.Rect; -import android.view.MotionEvent; -import android.view.SurfaceControl; -import android.view.View; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.TextView; -import android.window.SurfaceSyncGroup; - -import androidx.annotation.VisibleForTesting; - -import com.android.window.flags.Flags; -import com.android.wm.shell.R; -import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.splitscreen.SplitScreenController; -import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer; -import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer; - -/** - * Handle menu opened when the appropriate button is clicked on. - * - * Displays up to 3 pills that show the following: - * App Info: App name, app icon, and collapse button to close the menu. - * Windowing Options(Proto 2 only): Buttons to change windowing modes. - * Additional Options: Miscellaneous functions including screenshot and closing task. - */ -class HandleMenu { - private static final String TAG = "HandleMenu"; - private static final boolean SHOULD_SHOW_MORE_ACTIONS_PILL = false; - private final Context mContext; - private final DesktopModeWindowDecoration mParentDecor; - @VisibleForTesting - AdditionalViewContainer mHandleMenuViewContainer; - // Position of the handle menu used for laying out the handle view. - @VisibleForTesting - final PointF mHandleMenuPosition = new PointF(); - // With the introduction of {@link AdditionalSystemViewContainer}, {@link mHandleMenuPosition} - // may be in a different coordinate space than the input coordinates. Therefore, we still care - // about the menu's coordinates relative to the display as a whole, so we need to maintain - // those as well. - final Point mGlobalMenuPosition = new Point(); - private final boolean mShouldShowWindowingPill; - private final Bitmap mAppIconBitmap; - private final CharSequence mAppName; - private final View.OnClickListener mOnClickListener; - private final View.OnTouchListener mOnTouchListener; - private final RunningTaskInfo mTaskInfo; - private final DisplayController mDisplayController; - private final SplitScreenController mSplitScreenController; - private final int mLayoutResId; - private int mMarginMenuTop; - private int mMarginMenuStart; - private int mMenuHeight; - private int mMenuWidth; - private final int mCaptionHeight; - private HandleMenuAnimator mHandleMenuAnimator; - - - HandleMenu(DesktopModeWindowDecoration parentDecor, int layoutResId, - View.OnClickListener onClickListener, View.OnTouchListener onTouchListener, - Bitmap appIcon, CharSequence appName, DisplayController displayController, - SplitScreenController splitScreenController, boolean shouldShowWindowingPill, - int captionHeight) { - mParentDecor = parentDecor; - mContext = mParentDecor.mDecorWindowContext; - mTaskInfo = mParentDecor.mTaskInfo; - mDisplayController = displayController; - mSplitScreenController = splitScreenController; - mLayoutResId = layoutResId; - mOnClickListener = onClickListener; - mOnTouchListener = onTouchListener; - mAppIconBitmap = appIcon; - mAppName = appName; - mShouldShowWindowingPill = shouldShowWindowingPill; - mCaptionHeight = captionHeight; - loadHandleMenuDimensions(); - updateHandleMenuPillPositions(); - } - - void show() { - final SurfaceSyncGroup ssg = new SurfaceSyncGroup(TAG); - SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - - createHandleMenuViewContainer(t, ssg); - ssg.addTransaction(t); - ssg.markSyncReady(); - setupHandleMenu(); - animateHandleMenu(); - } - - private void createHandleMenuViewContainer(SurfaceControl.Transaction t, - SurfaceSyncGroup ssg) { - final int x = (int) mHandleMenuPosition.x; - final int y = (int) mHandleMenuPosition.y; - if (!mTaskInfo.isFreeform() && Flags.enableAdditionalWindowsAboveStatusBar()) { - mHandleMenuViewContainer = new AdditionalSystemViewContainer(mContext, - R.layout.desktop_mode_window_decor_handle_menu, mTaskInfo.taskId, - x, y, mMenuWidth, mMenuHeight); - } else { - mHandleMenuViewContainer = mParentDecor.addWindow( - R.layout.desktop_mode_window_decor_handle_menu, "Handle Menu", - t, ssg, x, y, mMenuWidth, mMenuHeight); - } - final View handleMenuView = mHandleMenuViewContainer.getView(); - mHandleMenuAnimator = new HandleMenuAnimator(handleMenuView, mMenuWidth, mCaptionHeight); - } - - /** - * Animates the appearance of the handle menu and its three pills. - */ - private void animateHandleMenu() { - if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN - || mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { - mHandleMenuAnimator.animateCaptionHandleExpandToOpen(); - } else { - mHandleMenuAnimator.animateOpen(); - } - } - - /** - * Set up all three pills of the handle menu: app info pill, windowing pill, & more actions - * pill. - */ - private void setupHandleMenu() { - final View handleMenu = mHandleMenuViewContainer.getView(); - handleMenu.setOnTouchListener(mOnTouchListener); - setupAppInfoPill(handleMenu); - if (mShouldShowWindowingPill) { - setupWindowingPill(handleMenu); - } - setupMoreActionsPill(handleMenu); - } - - /** - * Set up interactive elements of handle menu's app info pill. - */ - private void setupAppInfoPill(View handleMenu) { - final HandleMenuImageButton collapseBtn = - handleMenu.findViewById(R.id.collapse_menu_button); - final ImageView appIcon = handleMenu.findViewById(R.id.application_icon); - final TextView appName = handleMenu.findViewById(R.id.application_name); - collapseBtn.setOnClickListener(mOnClickListener); - collapseBtn.setTaskInfo(mTaskInfo); - appIcon.setImageBitmap(mAppIconBitmap); - appName.setText(mAppName); - } - - /** - * Set up interactive elements and color of handle menu's windowing pill. - */ - private void setupWindowingPill(View handleMenu) { - final ImageButton fullscreenBtn = handleMenu.findViewById( - R.id.fullscreen_button); - final ImageButton splitscreenBtn = handleMenu.findViewById( - R.id.split_screen_button); - final ImageButton floatingBtn = handleMenu.findViewById(R.id.floating_button); - // TODO: Remove once implemented. - floatingBtn.setVisibility(View.GONE); - - final ImageButton desktopBtn = handleMenu.findViewById(R.id.desktop_button); - fullscreenBtn.setOnClickListener(mOnClickListener); - splitscreenBtn.setOnClickListener(mOnClickListener); - floatingBtn.setOnClickListener(mOnClickListener); - desktopBtn.setOnClickListener(mOnClickListener); - // The button corresponding to the windowing mode that the task is currently in uses a - // different color than the others. - final ColorStateList[] iconColors = getWindowingIconColor(); - final ColorStateList inActiveColorStateList = iconColors[0]; - final ColorStateList activeColorStateList = iconColors[1]; - final int windowingMode = mTaskInfo.getWindowingMode(); - fullscreenBtn.setImageTintList(windowingMode == WINDOWING_MODE_FULLSCREEN - ? activeColorStateList : inActiveColorStateList); - splitscreenBtn.setImageTintList(windowingMode == WINDOWING_MODE_MULTI_WINDOW - ? activeColorStateList : inActiveColorStateList); - floatingBtn.setImageTintList(windowingMode == WINDOWING_MODE_PINNED - ? activeColorStateList : inActiveColorStateList); - desktopBtn.setImageTintList(windowingMode == WINDOWING_MODE_FREEFORM - ? activeColorStateList : inActiveColorStateList); - } - - /** - * Set up interactive elements & height of handle menu's more actions pill - */ - private void setupMoreActionsPill(View handleMenu) { - if (!SHOULD_SHOW_MORE_ACTIONS_PILL) { - handleMenu.findViewById(R.id.more_actions_pill).setVisibility(View.GONE); - } - } - - /** - * Returns array of windowing icon color based on current UI theme. First element of the - * array is for inactive icons and the second is for active icons. - */ - private ColorStateList[] getWindowingIconColor() { - final int mode = mContext.getResources().getConfiguration().uiMode - & Configuration.UI_MODE_NIGHT_MASK; - final boolean isNightMode = (mode == Configuration.UI_MODE_NIGHT_YES); - final TypedArray typedArray = mContext.obtainStyledAttributes(new int[]{ - com.android.internal.R.attr.materialColorOnSurface, - com.android.internal.R.attr.materialColorPrimary}); - final int inActiveColor = typedArray.getColor(0, isNightMode ? Color.WHITE : Color.BLACK); - final int activeColor = typedArray.getColor(1, isNightMode ? Color.WHITE : Color.BLACK); - typedArray.recycle(); - return new ColorStateList[]{ColorStateList.valueOf(inActiveColor), - ColorStateList.valueOf(activeColor)}; - } - - /** - * Updates handle menu's position variables to reflect its next position. - */ - private void updateHandleMenuPillPositions() { - int menuX; - final int menuY; - final Rect taskBounds = mTaskInfo.getConfiguration().windowConfiguration.getBounds(); - updateGlobalMenuPosition(taskBounds); - if (mLayoutResId == R.layout.desktop_mode_app_header) { - // Align the handle menu to the left side of the caption. - menuX = mMarginMenuStart; - menuY = mMarginMenuTop; - } else { - if (Flags.enableAdditionalWindowsAboveStatusBar()) { - // In a focused decor, we use global coordinates for handle menu. Therefore we - // need to account for other factors like split stage and menu/handle width to - // center the menu. - final DisplayLayout layout = mDisplayController - .getDisplayLayout(mTaskInfo.displayId); - menuX = mGlobalMenuPosition.x + ((mMenuWidth - layout.width()) / 2); - menuY = mGlobalMenuPosition.y + ((mMenuHeight - layout.height()) / 2); - } else { - menuX = (taskBounds.width() / 2) - (mMenuWidth / 2); - menuY = mMarginMenuTop; - } - } - // Handle Menu position setup. - mHandleMenuPosition.set(menuX, menuY); - } - - private void updateGlobalMenuPosition(Rect taskBounds) { - if (mTaskInfo.isFreeform()) { - mGlobalMenuPosition.set(taskBounds.left + mMarginMenuStart, - taskBounds.top + mMarginMenuTop); - } else if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { - mGlobalMenuPosition.set( - (taskBounds.width() / 2) - (mMenuWidth / 2) + mMarginMenuStart, - mMarginMenuTop - ); - } else if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { - final int splitPosition = mSplitScreenController.getSplitPosition(mTaskInfo.taskId); - final Rect leftOrTopStageBounds = new Rect(); - final Rect rightOrBottomStageBounds = new Rect(); - mSplitScreenController.getStageBounds(leftOrTopStageBounds, - rightOrBottomStageBounds); - // TODO(b/343561161): This needs to be calculated differently if the task is in - // top/bottom split. - if (splitPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT) { - mGlobalMenuPosition.set(leftOrTopStageBounds.width() - + (rightOrBottomStageBounds.width() / 2) - - (mMenuWidth / 2) + mMarginMenuStart, - mMarginMenuTop); - } else if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) { - mGlobalMenuPosition.set((leftOrTopStageBounds.width() / 2) - - (mMenuWidth / 2) + mMarginMenuStart, - mMarginMenuTop); - } - } - } - - /** - * Update pill layout, in case task changes have caused positioning to change. - */ - void relayout(SurfaceControl.Transaction t) { - if (mHandleMenuViewContainer != null) { - updateHandleMenuPillPositions(); - mHandleMenuViewContainer.setPosition(t, mHandleMenuPosition.x, mHandleMenuPosition.y); - } - } - - /** - * Check a passed MotionEvent if a click or hover has occurred on any button on this caption - * Note this should only be called when a regular onClick/onHover is not possible - * (i.e. the button was clicked through status bar layer) - * - * @param ev the MotionEvent to compare against. - */ - void checkMotionEvent(MotionEvent ev) { - // If the menu view is above status bar, we can let the views handle input directly. - if (isViewAboveStatusBar()) return; - final View handleMenu = mHandleMenuViewContainer.getView(); - final HandleMenuImageButton collapse = handleMenu.findViewById(R.id.collapse_menu_button); - final PointF inputPoint = translateInputToLocalSpace(ev); - final boolean inputInCollapseButton = pointInView(collapse, inputPoint.x, inputPoint.y); - final int action = ev.getActionMasked(); - collapse.setHovered(inputInCollapseButton && action != ACTION_UP); - collapse.setPressed(inputInCollapseButton && action == ACTION_DOWN); - if (action == ACTION_UP && inputInCollapseButton) { - collapse.performClick(); - } - } - - private boolean isViewAboveStatusBar() { - return Flags.enableAdditionalWindowsAboveStatusBar() - && !mTaskInfo.isFreeform(); - } - - // Translate the input point from display coordinates to the same space as the handle menu. - private PointF translateInputToLocalSpace(MotionEvent ev) { - return new PointF(ev.getX() - mHandleMenuPosition.x, - ev.getY() - mHandleMenuPosition.y); - } - - /** - * A valid menu input is one of the following: - * An input that happens in the menu views. - * Any input before the views have been laid out. - * - * @param inputPoint the input to compare against. - */ - boolean isValidMenuInput(PointF inputPoint) { - if (!viewsLaidOut()) return true; - if (!isViewAboveStatusBar()) { - return pointInView( - mHandleMenuViewContainer.getView(), - inputPoint.x - mHandleMenuPosition.x, - inputPoint.y - mHandleMenuPosition.y); - } else { - // Handle menu exists in a different coordinate space when added to WindowManager. - // Therefore we must compare the provided input coordinates to global menu coordinates. - // This includes factoring for split stage as input coordinates are relative to split - // stage position, not relative to the display as a whole. - PointF inputRelativeToMenu = new PointF( - inputPoint.x - mGlobalMenuPosition.x, - inputPoint.y - mGlobalMenuPosition.y - ); - if (mSplitScreenController.getSplitPosition(mTaskInfo.taskId) - == SPLIT_POSITION_BOTTOM_OR_RIGHT) { - // TODO(b/343561161): This also needs to be calculated differently if - // the task is in top/bottom split. - Rect leftStageBounds = new Rect(); - mSplitScreenController.getStageBounds(leftStageBounds, new Rect()); - inputRelativeToMenu.x += leftStageBounds.width(); - } - return pointInView( - mHandleMenuViewContainer.getView(), - inputRelativeToMenu.x, - inputRelativeToMenu.y); - } - } - - private boolean pointInView(View v, float x, float y) { - return v != null && v.getLeft() <= x && v.getRight() >= x - && v.getTop() <= y && v.getBottom() >= y; - } - - /** - * Check if the views for handle menu can be seen. - */ - private boolean viewsLaidOut() { - return mHandleMenuViewContainer.getView().isLaidOut(); - } - - private void loadHandleMenuDimensions() { - final Resources resources = mContext.getResources(); - mMenuWidth = loadDimensionPixelSize(resources, - R.dimen.desktop_mode_handle_menu_width); - mMenuHeight = getHandleMenuHeight(resources); - mMarginMenuTop = loadDimensionPixelSize(resources, - R.dimen.desktop_mode_handle_menu_margin_top); - mMarginMenuStart = loadDimensionPixelSize(resources, - R.dimen.desktop_mode_handle_menu_margin_start); - } - - /** - * Determines handle menu height based on if windowing pill should be shown. - */ - private int getHandleMenuHeight(Resources resources) { - int menuHeight = loadDimensionPixelSize(resources, R.dimen.desktop_mode_handle_menu_height); - if (!mShouldShowWindowingPill) { - menuHeight -= loadDimensionPixelSize(resources, - R.dimen.desktop_mode_handle_menu_windowing_pill_height); - } - if (!SHOULD_SHOW_MORE_ACTIONS_PILL) { - menuHeight -= loadDimensionPixelSize(resources, - R.dimen.desktop_mode_handle_menu_more_actions_pill_height); - } - return menuHeight; - } - - private int loadDimensionPixelSize(Resources resources, int resourceId) { - if (resourceId == Resources.ID_NULL) { - return 0; - } - return resources.getDimensionPixelSize(resourceId); - } - - void close() { - final Runnable after = () -> { - mHandleMenuViewContainer.releaseView(); - mHandleMenuViewContainer = null; - }; - if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN - || mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { - mHandleMenuAnimator.animateCollapseIntoHandleClose(after); - } else { - mHandleMenuAnimator.animateClose(after); - } - } - - static final class Builder { - private final DesktopModeWindowDecoration mParent; - private CharSequence mName; - private Bitmap mAppIcon; - private View.OnClickListener mOnClickListener; - private View.OnTouchListener mOnTouchListener; - private int mLayoutId; - private boolean mShowWindowingPill; - private int mCaptionHeight; - private DisplayController mDisplayController; - private SplitScreenController mSplitScreenController; - - Builder(@NonNull DesktopModeWindowDecoration parent) { - mParent = parent; - } - - Builder setAppName(@Nullable CharSequence name) { - mName = name; - return this; - } - - Builder setAppIcon(@Nullable Bitmap appIcon) { - mAppIcon = appIcon; - return this; - } - - Builder setOnClickListener(@Nullable View.OnClickListener onClickListener) { - mOnClickListener = onClickListener; - return this; - } - - Builder setOnTouchListener(@Nullable View.OnTouchListener onTouchListener) { - mOnTouchListener = onTouchListener; - return this; - } - - Builder setLayoutId(int layoutId) { - mLayoutId = layoutId; - return this; - } - - Builder setWindowingButtonsVisible(boolean windowingButtonsVisible) { - mShowWindowingPill = windowingButtonsVisible; - return this; - } - - Builder setCaptionHeight(int captionHeight) { - mCaptionHeight = captionHeight; - return this; - } - - Builder setDisplayController(DisplayController displayController) { - mDisplayController = displayController; - return this; - } - - Builder setSplitScreenController(SplitScreenController splitScreenController) { - mSplitScreenController = splitScreenController; - return this; - } - - HandleMenu build() { - return new HandleMenu(mParent, mLayoutId, mOnClickListener, - mOnTouchListener, mAppIcon, mName, mDisplayController, mSplitScreenController, - mShowWindowingPill, mCaptionHeight); - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt new file mode 100644 index 000000000000..9a5b4f54dd36 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -0,0 +1,704 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.windowdecor + +import android.annotation.ColorInt +import android.annotation.DimenRes +import android.annotation.SuppressLint +import android.app.ActivityManager.RunningTaskInfo +import android.content.Context +import android.content.res.ColorStateList +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Point +import android.graphics.PointF +import android.graphics.Rect +import android.net.Uri +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.MotionEvent.ACTION_OUTSIDE +import android.view.SurfaceControl +import android.view.View +import android.view.WindowManager +import android.widget.Button +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import android.window.SurfaceSyncGroup +import androidx.annotation.VisibleForTesting +import androidx.compose.ui.graphics.toArgb +import androidx.core.view.isGone +import com.android.window.flags.Flags +import com.android.wm.shell.R +import com.android.wm.shell.shared.split.SplitScreenConstants +import com.android.wm.shell.splitscreen.SplitScreenController +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer +import com.android.wm.shell.windowdecor.common.DecorThemeUtil +import com.android.wm.shell.windowdecor.extension.isFullscreen +import com.android.wm.shell.windowdecor.extension.isMultiWindow +import com.android.wm.shell.windowdecor.extension.isPinned + +/** + * Handle menu opened when the appropriate button is clicked on. + * + * Displays up to 3 pills that show the following: + * App Info: App name, app icon, and collapse button to close the menu. + * Windowing Options(Proto 2 only): Buttons to change windowing modes. + * Additional Options: Miscellaneous functions including screenshot and closing task. + */ +class HandleMenu( + private val parentDecor: DesktopModeWindowDecoration, + private val windowManagerWrapper: WindowManagerWrapper, + private val layoutResId: Int, + private val appIconBitmap: Bitmap?, + private val appName: CharSequence?, + private val splitScreenController: SplitScreenController, + private val shouldShowWindowingPill: Boolean, + private val shouldShowNewWindowButton: Boolean, + private val shouldShowManageWindowsButton: Boolean, + private val openInBrowserLink: Uri?, + private val captionWidth: Int, + private val captionHeight: Int, + captionX: Int +) { + private val context: Context = parentDecor.mDecorWindowContext + private val taskInfo: RunningTaskInfo = parentDecor.mTaskInfo + + private val isViewAboveStatusBar: Boolean + get() = (Flags.enableHandleInputFix() && !taskInfo.isFreeform) + + private val pillElevation: Int = loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_pill_elevation) + private val pillTopMargin: Int = loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_pill_spacing_margin) + private val menuWidth = loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_width) + pillElevation + private val menuHeight = getHandleMenuHeight() + private val marginMenuTop = loadDimensionPixelSize(R.dimen.desktop_mode_handle_menu_margin_top) + private val marginMenuStart = loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_margin_start) + + @VisibleForTesting + var handleMenuViewContainer: AdditionalViewContainer? = null + private var handleMenuView: HandleMenuView? = null + + // Position of the handle menu used for laying out the handle view. + @VisibleForTesting + val handleMenuPosition: PointF = PointF() + + // With the introduction of {@link AdditionalSystemViewContainer}, {@link mHandleMenuPosition} + // may be in a different coordinate space than the input coordinates. Therefore, we still care + // about the menu's coordinates relative to the display as a whole, so we need to maintain + // those as well. + private val globalMenuPosition: Point = Point() + + private val shouldShowBrowserPill: Boolean + get() = openInBrowserLink != null + + init { + updateHandleMenuPillPositions(captionX) + } + + fun show( + onToDesktopClickListener: () -> Unit, + onToFullscreenClickListener: () -> Unit, + onToSplitScreenClickListener: () -> Unit, + onNewWindowClickListener: () -> Unit, + onManageWindowsClickListener: () -> Unit, + openInBrowserClickListener: (Uri) -> Unit, + onCloseMenuClickListener: () -> Unit, + onOutsideTouchListener: () -> Unit, + ) { + val ssg = SurfaceSyncGroup(TAG) + val t = SurfaceControl.Transaction() + + createHandleMenu( + t = t, + ssg = ssg, + onToDesktopClickListener = onToDesktopClickListener, + onToFullscreenClickListener = onToFullscreenClickListener, + onToSplitScreenClickListener = onToSplitScreenClickListener, + onNewWindowClickListener = onNewWindowClickListener, + onManageWindowsClickListener = onManageWindowsClickListener, + openInBrowserClickListener = openInBrowserClickListener, + onCloseMenuClickListener = onCloseMenuClickListener, + onOutsideTouchListener = onOutsideTouchListener, + ) + ssg.addTransaction(t) + ssg.markSyncReady() + + handleMenuView?.animateOpenMenu() + } + + private fun createHandleMenu( + t: SurfaceControl.Transaction, + ssg: SurfaceSyncGroup, + onToDesktopClickListener: () -> Unit, + onToFullscreenClickListener: () -> Unit, + onToSplitScreenClickListener: () -> Unit, + onNewWindowClickListener: () -> Unit, + onManageWindowsClickListener: () -> Unit, + openInBrowserClickListener: (Uri) -> Unit, + onCloseMenuClickListener: () -> Unit, + onOutsideTouchListener: () -> Unit + ) { + val handleMenuView = HandleMenuView( + context = context, + menuWidth = menuWidth, + captionHeight = captionHeight, + shouldShowWindowingPill = shouldShowWindowingPill, + shouldShowBrowserPill = shouldShowBrowserPill, + shouldShowNewWindowButton = shouldShowNewWindowButton, + shouldShowManageWindowsButton = shouldShowManageWindowsButton + ).apply { + bind(taskInfo, appIconBitmap, appName) + this.onToDesktopClickListener = onToDesktopClickListener + this.onToFullscreenClickListener = onToFullscreenClickListener + this.onToSplitScreenClickListener = onToSplitScreenClickListener + this.onNewWindowClickListener = onNewWindowClickListener + this.onManageWindowsClickListener = onManageWindowsClickListener + this.onOpenInBrowserClickListener = { + openInBrowserClickListener.invoke(openInBrowserLink!!) + } + this.onCloseMenuClickListener = onCloseMenuClickListener + this.onOutsideTouchListener = onOutsideTouchListener + } + + val x = handleMenuPosition.x.toInt() + val y = handleMenuPosition.y.toInt() + handleMenuViewContainer = + if (!taskInfo.isFreeform && Flags.enableHandleInputFix()) { + AdditionalSystemViewContainer( + windowManagerWrapper = windowManagerWrapper, + taskId = taskInfo.taskId, + x = x, + y = y, + width = menuWidth, + height = menuHeight, + flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or + WindowManager.LayoutParams.FLAG_SPLIT_TOUCH, + view = handleMenuView.rootView + ) + } else { + parentDecor.addWindow( + handleMenuView.rootView, "Handle Menu", t, ssg, x, y, menuWidth, menuHeight + ) + } + + this.handleMenuView = handleMenuView + } + + /** + * Updates handle menu's position variables to reflect its next position. + */ + private fun updateHandleMenuPillPositions(captionX: Int) { + val menuX: Int + val menuY: Int + val taskBounds = taskInfo.getConfiguration().windowConfiguration.bounds + updateGlobalMenuPosition(taskBounds, captionX) + if (layoutResId == R.layout.desktop_mode_app_header) { + // Align the handle menu to the left side of the caption. + menuX = marginMenuStart + menuY = marginMenuTop + } else { + if (Flags.enableHandleInputFix()) { + // In a focused decor, we use global coordinates for handle menu. Therefore we + // need to account for other factors like split stage and menu/handle width to + // center the menu. + menuX = globalMenuPosition.x + menuY = globalMenuPosition.y + } else { + menuX = (taskBounds.width() / 2) - (menuWidth / 2) + menuY = marginMenuTop + } + } + // Handle Menu position setup. + handleMenuPosition.set(menuX.toFloat(), menuY.toFloat()) + } + + private fun updateGlobalMenuPosition(taskBounds: Rect, captionX: Int) { + val nonFreeformX = captionX + (captionWidth / 2) - (menuWidth / 2) + when { + taskInfo.isFreeform -> { + globalMenuPosition.set( + /* x = */ taskBounds.left + marginMenuStart, + /* y = */ taskBounds.top + marginMenuTop + ) + } + taskInfo.isFullscreen -> { + globalMenuPosition.set( + /* x = */ nonFreeformX, + /* y = */ marginMenuTop + ) + } + taskInfo.isMultiWindow -> { + val splitPosition = splitScreenController.getSplitPosition(taskInfo.taskId) + val leftOrTopStageBounds = Rect() + val rightOrBottomStageBounds = Rect() + splitScreenController.getStageBounds(leftOrTopStageBounds, rightOrBottomStageBounds) + // TODO(b/343561161): This needs to be calculated differently if the task is in + // top/bottom split. + when (splitPosition) { + SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT -> { + globalMenuPosition.set( + /* x = */ leftOrTopStageBounds.width() + nonFreeformX, + /* y = */ marginMenuTop + ) + } + SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT -> { + globalMenuPosition.set( + /* x = */ nonFreeformX, + /* y = */ marginMenuTop + ) + } + } + } + } + } + + /** + * Update pill layout, in case task changes have caused positioning to change. + */ + fun relayout( + t: SurfaceControl.Transaction, + captionX: Int + ) { + handleMenuViewContainer?.let { container -> + updateHandleMenuPillPositions(captionX) + container.setPosition(t, handleMenuPosition.x, handleMenuPosition.y) + } + } + + /** + * Check a passed MotionEvent if a click or hover has occurred on any button on this caption + * Note this should only be called when a regular onClick/onHover is not possible + * (i.e. the button was clicked through status bar layer) + * + * @param ev the MotionEvent to compare against. + */ + fun checkMotionEvent(ev: MotionEvent) { + // If the menu view is above status bar, we can let the views handle input directly. + if (isViewAboveStatusBar) return + val inputPoint = translateInputToLocalSpace(ev) + handleMenuView?.checkMotionEvent(ev, inputPoint) + } + + // Translate the input point from display coordinates to the same space as the handle menu. + private fun translateInputToLocalSpace(ev: MotionEvent): PointF { + return PointF( + ev.x - handleMenuPosition.x, + ev.y - handleMenuPosition.y + ) + } + + /** + * A valid menu input is one of the following: + * An input that happens in the menu views. + * Any input before the views have been laid out. + * + * @param inputPoint the input to compare against. + */ + fun isValidMenuInput(inputPoint: PointF): Boolean { + if (!viewsLaidOut()) return true + if (!isViewAboveStatusBar) { + return pointInView( + handleMenuViewContainer?.view, + inputPoint.x - handleMenuPosition.x, + inputPoint.y - handleMenuPosition.y + ) + } else { + // Handle menu exists in a different coordinate space when added to WindowManager. + // Therefore we must compare the provided input coordinates to global menu coordinates. + // This includes factoring for split stage as input coordinates are relative to split + // stage position, not relative to the display as a whole. + val inputRelativeToMenu = PointF( + inputPoint.x - globalMenuPosition.x, + inputPoint.y - globalMenuPosition.y + ) + if (splitScreenController.getSplitPosition(taskInfo.taskId) + == SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT) { + // TODO(b/343561161): This also needs to be calculated differently if + // the task is in top/bottom split. + val leftStageBounds = Rect() + splitScreenController.getStageBounds(leftStageBounds, Rect()) + inputRelativeToMenu.x += leftStageBounds.width().toFloat() + } + return pointInView( + handleMenuViewContainer?.view, + inputRelativeToMenu.x, + inputRelativeToMenu.y + ) + } + } + + private fun pointInView(v: View?, x: Float, y: Float): Boolean { + return v != null && v.left <= x && v.right >= x && v.top <= y && v.bottom >= y + } + + /** + * Check if the views for handle menu can be seen. + */ + private fun viewsLaidOut(): Boolean = handleMenuViewContainer?.view?.isLaidOut ?: false + + /** + * Determines handle menu height based the max size and the visibility of pills. + */ + private fun getHandleMenuHeight(): Int { + var menuHeight = loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_height) + pillElevation + if (!shouldShowWindowingPill) { + menuHeight -= loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_windowing_pill_height) + menuHeight -= pillTopMargin + } + if (!SHOULD_SHOW_SCREENSHOT_BUTTON) { + menuHeight -= loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_screenshot_height + ) + } + if (!shouldShowNewWindowButton) { + menuHeight -= loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_new_window_height + ) + } + if (!shouldShowManageWindowsButton) { + menuHeight -= loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_manage_windows_height + ) + } + if (!SHOULD_SHOW_SCREENSHOT_BUTTON && !shouldShowNewWindowButton + && !shouldShowManageWindowsButton) { + menuHeight -= pillTopMargin + } + if (!shouldShowBrowserPill) { + menuHeight -= loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_open_in_browser_pill_height) + menuHeight -= pillTopMargin + } + return menuHeight + } + + private fun loadDimensionPixelSize(@DimenRes resourceId: Int): Int { + if (resourceId == Resources.ID_NULL) { + return 0 + } + return context.resources.getDimensionPixelSize(resourceId) + } + + fun close() { + handleMenuView?.animateCloseMenu { + handleMenuViewContainer?.releaseView() + handleMenuViewContainer = null + } + } + + /** The view within the Handle Menu, with options to change the windowing mode and more. */ + @SuppressLint("ClickableViewAccessibility") + class HandleMenuView( + context: Context, + menuWidth: Int, + captionHeight: Int, + private val shouldShowWindowingPill: Boolean, + private val shouldShowBrowserPill: Boolean, + private val shouldShowNewWindowButton: Boolean, + private val shouldShowManageWindowsButton: Boolean + ) { + val rootView = LayoutInflater.from(context) + .inflate(R.layout.desktop_mode_window_decor_handle_menu, null /* root */) as View + + // App Info Pill. + private val appInfoPill = rootView.requireViewById<View>(R.id.app_info_pill) + private val collapseMenuButton = appInfoPill.requireViewById<HandleMenuImageButton>( + R.id.collapse_menu_button) + private val appIconView = appInfoPill.requireViewById<ImageView>(R.id.application_icon) + private val appNameView = appInfoPill.requireViewById<TextView>(R.id.application_name) + + // Windowing Pill. + private val windowingPill = rootView.requireViewById<View>(R.id.windowing_pill) + private val fullscreenBtn = windowingPill.requireViewById<ImageButton>( + R.id.fullscreen_button) + private val splitscreenBtn = windowingPill.requireViewById<ImageButton>( + R.id.split_screen_button) + private val floatingBtn = windowingPill.requireViewById<ImageButton>(R.id.floating_button) + private val desktopBtn = windowingPill.requireViewById<ImageButton>(R.id.desktop_button) + + // More Actions Pill. + private val moreActionsPill = rootView.requireViewById<View>(R.id.more_actions_pill) + private val screenshotBtn = moreActionsPill.requireViewById<Button>(R.id.screenshot_button) + private val newWindowBtn = moreActionsPill.requireViewById<Button>(R.id.new_window_button) + private val manageWindowBtn = moreActionsPill + .requireViewById<Button>(R.id.manage_windows_button) + + // Open in Browser Pill. + private val openInBrowserPill = rootView.requireViewById<View>(R.id.open_in_browser_pill) + private val browserBtn = openInBrowserPill.requireViewById<Button>( + R.id.open_in_browser_button) + + private val decorThemeUtil = DecorThemeUtil(context) + private val animator = HandleMenuAnimator(rootView, menuWidth, captionHeight.toFloat()) + + private lateinit var taskInfo: RunningTaskInfo + private lateinit var style: MenuStyle + + var onToDesktopClickListener: (() -> Unit)? = null + var onToFullscreenClickListener: (() -> Unit)? = null + var onToSplitScreenClickListener: (() -> Unit)? = null + var onNewWindowClickListener: (() -> Unit)? = null + var onManageWindowsClickListener: (() -> Unit)? = null + var onOpenInBrowserClickListener: (() -> Unit)? = null + var onCloseMenuClickListener: (() -> Unit)? = null + var onOutsideTouchListener: (() -> Unit)? = null + + init { + fullscreenBtn.setOnClickListener { onToFullscreenClickListener?.invoke() } + splitscreenBtn.setOnClickListener { onToSplitScreenClickListener?.invoke() } + desktopBtn.setOnClickListener { onToDesktopClickListener?.invoke() } + browserBtn.setOnClickListener { onOpenInBrowserClickListener?.invoke() } + collapseMenuButton.setOnClickListener { onCloseMenuClickListener?.invoke() } + newWindowBtn.setOnClickListener { onNewWindowClickListener?.invoke() } + manageWindowBtn.setOnClickListener { onManageWindowsClickListener?.invoke() } + + rootView.setOnTouchListener { _, event -> + if (event.actionMasked == ACTION_OUTSIDE) { + onOutsideTouchListener?.invoke() + return@setOnTouchListener false + } + return@setOnTouchListener true + } + } + + /** Binds the menu views to the new data. */ + fun bind(taskInfo: RunningTaskInfo, appIconBitmap: Bitmap?, appName: CharSequence?) { + this.taskInfo = taskInfo + this.style = calculateMenuStyle(taskInfo) + + bindAppInfoPill(style, appIconBitmap, appName) + if (shouldShowWindowingPill) { + bindWindowingPill(style) + } + bindMoreActionsPill(style) + bindOpenInBrowserPill(style) + } + + /** Animates the menu opening. */ + fun animateOpenMenu() { + if (taskInfo.isFullscreen || taskInfo.isMultiWindow) { + animator.animateCaptionHandleExpandToOpen() + } else { + animator.animateOpen() + } + } + + /** Animates the menu closing. */ + fun animateCloseMenu(onAnimFinish: () -> Unit) { + if (taskInfo.isFullscreen || taskInfo.isMultiWindow) { + animator.animateCollapseIntoHandleClose(onAnimFinish) + } else { + animator.animateClose(onAnimFinish) + } + } + + /** + * Checks whether a motion event falls inside this menu, and invokes a click of the + * collapse button if needed. + * Note: should only be called when regular click detection doesn't work because input is + * detected through the status bar layer with a global input monitor. + */ + fun checkMotionEvent(ev: MotionEvent, inputPointLocal: PointF) { + val inputInCollapseButton = pointInView( + collapseMenuButton, + inputPointLocal.x, + inputPointLocal.y + ) + val action = ev.actionMasked + collapseMenuButton.isHovered = inputInCollapseButton + && action != MotionEvent.ACTION_UP + collapseMenuButton.isPressed = inputInCollapseButton + && action == MotionEvent.ACTION_DOWN + if (action == MotionEvent.ACTION_UP && inputInCollapseButton) { + collapseMenuButton.performClick() + } + } + + private fun pointInView(v: View?, x: Float, y: Float): Boolean { + return v != null && v.left <= x && v.right >= x && v.top <= y && v.bottom >= y + } + + private fun calculateMenuStyle(taskInfo: RunningTaskInfo): MenuStyle { + val colorScheme = decorThemeUtil.getColorScheme(taskInfo) + return MenuStyle( + backgroundColor = colorScheme.surfaceBright.toArgb(), + textColor = colorScheme.onSurface.toArgb(), + windowingButtonColor = ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_pressed), + intArrayOf(android.R.attr.state_focused), + intArrayOf(android.R.attr.state_selected), + intArrayOf(), + ), + intArrayOf( + colorScheme.onSurface.toArgb(), + colorScheme.onSurface.toArgb(), + colorScheme.primary.toArgb(), + colorScheme.onSurface.toArgb(), + ) + ), + ) + } + + private fun bindAppInfoPill( + style: MenuStyle, + appIconBitmap: Bitmap?, + appName: CharSequence? + ) { + appInfoPill.background.setTint(style.backgroundColor) + + collapseMenuButton.apply { + imageTintList = ColorStateList.valueOf(style.textColor) + this.taskInfo = this@HandleMenuView.taskInfo + } + appIconView.setImageBitmap(appIconBitmap) + appNameView.apply { + text = appName + setTextColor(style.textColor) + } + } + + private fun bindWindowingPill(style: MenuStyle) { + windowingPill.background.setTint(style.backgroundColor) + + // TODO: Remove once implemented. + floatingBtn.visibility = View.GONE + + fullscreenBtn.isSelected = taskInfo.isFullscreen + fullscreenBtn.isEnabled = !taskInfo.isFullscreen + fullscreenBtn.imageTintList = style.windowingButtonColor + splitscreenBtn.isSelected = taskInfo.isMultiWindow + splitscreenBtn.isEnabled = !taskInfo.isMultiWindow + splitscreenBtn.imageTintList = style.windowingButtonColor + floatingBtn.isSelected = taskInfo.isPinned + floatingBtn.isEnabled = !taskInfo.isPinned + floatingBtn.imageTintList = style.windowingButtonColor + desktopBtn.isSelected = taskInfo.isFreeform + desktopBtn.isEnabled = !taskInfo.isFreeform + desktopBtn.imageTintList = style.windowingButtonColor + } + + private fun bindMoreActionsPill(style: MenuStyle) { + moreActionsPill.apply { + isGone = !shouldShowNewWindowButton && !SHOULD_SHOW_SCREENSHOT_BUTTON + && !shouldShowManageWindowsButton + } + screenshotBtn.apply { + isGone = !SHOULD_SHOW_SCREENSHOT_BUTTON + background.setTint(style.backgroundColor) + setTextColor(style.textColor) + compoundDrawableTintList = ColorStateList.valueOf(style.textColor) + } + newWindowBtn.apply { + isGone = !shouldShowNewWindowButton + background.setTint(style.backgroundColor) + setTextColor(style.textColor) + compoundDrawableTintList = ColorStateList.valueOf(style.textColor) + } + manageWindowBtn.apply { + isGone = !shouldShowManageWindowsButton + background.setTint(style.backgroundColor) + setTextColor(style.textColor) + compoundDrawableTintList = ColorStateList.valueOf(style.textColor) + } + } + + private fun bindOpenInBrowserPill(style: MenuStyle) { + openInBrowserPill.apply { + isGone = !shouldShowBrowserPill + background.setTint(style.backgroundColor) + } + + browserBtn.apply { + setTextColor(style.textColor) + compoundDrawableTintList = ColorStateList.valueOf(style.textColor) + } + } + + private data class MenuStyle( + @ColorInt val backgroundColor: Int, + @ColorInt val textColor: Int, + val windowingButtonColor: ColorStateList, + ) + } + + companion object { + private const val TAG = "HandleMenu" + private const val SHOULD_SHOW_SCREENSHOT_BUTTON = false + } +} + +/** A factory interface to create a [HandleMenu]. */ +interface HandleMenuFactory { + fun create( + parentDecor: DesktopModeWindowDecoration, + windowManagerWrapper: WindowManagerWrapper, + layoutResId: Int, + appIconBitmap: Bitmap?, + appName: CharSequence?, + splitScreenController: SplitScreenController, + shouldShowWindowingPill: Boolean, + shouldShowNewWindowButton: Boolean, + shouldShowManageWindowsButton: Boolean, + openInBrowserLink: Uri?, + captionWidth: Int, + captionHeight: Int, + captionX: Int + ): HandleMenu +} + +/** A [HandleMenuFactory] implementation that creates a [HandleMenu]. */ +object DefaultHandleMenuFactory : HandleMenuFactory { + override fun create( + parentDecor: DesktopModeWindowDecoration, + windowManagerWrapper: WindowManagerWrapper, + layoutResId: Int, + appIconBitmap: Bitmap?, + appName: CharSequence?, + splitScreenController: SplitScreenController, + shouldShowWindowingPill: Boolean, + shouldShowNewWindowButton: Boolean, + shouldShowManageWindowsButton: Boolean, + openInBrowserLink: Uri?, + captionWidth: Int, + captionHeight: Int, + captionX: Int + ): HandleMenu { + return HandleMenu( + parentDecor, + windowManagerWrapper, + layoutResId, + appIconBitmap, + appName, + splitScreenController, + shouldShowWindowingPill, + shouldShowNewWindowButton, + shouldShowManageWindowsButton, + openInBrowserLink, + captionWidth, + captionHeight, + captionX + ) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt index 8c5d4a2c2ffb..0c475f12f53b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt @@ -26,10 +26,12 @@ import android.view.View.SCALE_Y import android.view.View.TRANSLATION_Y import android.view.View.TRANSLATION_Z import android.view.ViewGroup +import android.view.accessibility.AccessibilityEvent +import android.widget.Button import androidx.core.animation.doOnEnd import androidx.core.view.children import com.android.wm.shell.R -import com.android.wm.shell.animation.Interpolators +import com.android.wm.shell.shared.animation.Interpolators /** Animates the Handle Menu opening. */ class HandleMenuAnimator( @@ -72,6 +74,7 @@ class HandleMenuAnimator( private val appInfoPill: ViewGroup = handleMenu.requireViewById(R.id.app_info_pill) private val windowingPill: ViewGroup = handleMenu.requireViewById(R.id.windowing_pill) private val moreActionsPill: ViewGroup = handleMenu.requireViewById(R.id.more_actions_pill) + private val openInBrowserPill: ViewGroup = handleMenu.requireViewById(R.id.open_in_browser_pill) /** Animates the opening of the handle menu. */ fun animateOpen() { @@ -80,7 +83,13 @@ class HandleMenuAnimator( animateAppInfoPillOpen() animateWindowingPillOpen() animateMoreActionsPillOpen() - runAnimations() + animateOpenInBrowserPill() + runAnimations { + appInfoPill.post { + appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent( + AccessibilityEvent.TYPE_VIEW_FOCUSED) + } + } } /** @@ -94,7 +103,13 @@ class HandleMenuAnimator( animateAppInfoPillOpen() animateWindowingPillOpen() animateMoreActionsPillOpen() - runAnimations() + animateOpenInBrowserPill() + runAnimations { + appInfoPill.post { + appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent( + AccessibilityEvent.TYPE_VIEW_FOCUSED) + } + } } /** @@ -104,11 +119,12 @@ class HandleMenuAnimator( * * @param after runs after the animation finishes. */ - fun animateCollapseIntoHandleClose(after: Runnable) { + fun animateCollapseIntoHandleClose(after: () -> Unit) { appInfoCollapseToHandle() animateAppInfoPillFadeOut() windowingPillClose() moreActionsPillClose() + openInBrowserPillClose() runAnimations(after) } @@ -120,11 +136,12 @@ class HandleMenuAnimator( * @param after runs after animation finishes. * */ - fun animateClose(after: Runnable) { + fun animateClose(after: () -> Unit) { appInfoPillCollapse() animateAppInfoPillFadeOut() windowingPillClose() moreActionsPillClose() + openInBrowserPillClose() runAnimations(after) } @@ -137,6 +154,7 @@ class HandleMenuAnimator( appInfoPill.children.forEach { it.alpha = 0f } windowingPill.alpha = 0f moreActionsPill.alpha = 0f + openInBrowserPill.alpha = 0f // Setup pivots. handleMenu.pivotX = menuWidth / 2f @@ -147,6 +165,9 @@ class HandleMenuAnimator( moreActionsPill.pivotX = menuWidth / 2f moreActionsPill.pivotY = appInfoPill.measuredHeight.toFloat() + + openInBrowserPill.pivotX = menuWidth / 2f + openInBrowserPill.pivotY = appInfoPill.measuredHeight.toFloat() } private fun animateAppInfoPillOpen() { @@ -268,12 +289,50 @@ class HandleMenuAnimator( // More Actions Content Opacity Animation moreActionsPill.children.forEach { animators += - ObjectAnimator.ofFloat(it, ALPHA, 1f).apply { + ObjectAnimator.ofFloat(it, ALPHA, 1f).apply { + startDelay = BODY_ALPHA_OPEN_DELAY + duration = BODY_CONTENT_ALPHA_OPEN_DURATION + interpolator = Interpolators.FAST_OUT_SLOW_IN + } + } + } + + private fun animateOpenInBrowserPill() { + // Open in Browser X & Y Scaling Animation + animators += + ObjectAnimator.ofFloat(openInBrowserPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply { + startDelay = BODY_SCALE_OPEN_DELAY + duration = BODY_SCALE_OPEN_DURATION + } + + animators += + ObjectAnimator.ofFloat(openInBrowserPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply { + startDelay = BODY_SCALE_OPEN_DELAY + duration = BODY_SCALE_OPEN_DURATION + } + + // Open in Browser Opacity Animation + animators += + ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 1f).apply { + startDelay = BODY_ALPHA_OPEN_DELAY + duration = BODY_ALPHA_OPEN_DURATION + } + + // Open in Browser Elevation Animation + animators += + ObjectAnimator.ofFloat(openInBrowserPill, TRANSLATION_Z, 1f).apply { + startDelay = ELEVATION_OPEN_DELAY + duration = BODY_ELEVATION_OPEN_DURATION + } + + // Open in Browser Button Opacity Animation + val button = openInBrowserPill.requireViewById<Button>(R.id.open_in_browser_button) + animators += + ObjectAnimator.ofFloat(button, ALPHA, 1f).apply { startDelay = BODY_ALPHA_OPEN_DELAY duration = BODY_CONTENT_ALPHA_OPEN_DURATION interpolator = Interpolators.FAST_OUT_SLOW_IN } - } } private fun appInfoPillCollapse() { @@ -379,14 +438,45 @@ class HandleMenuAnimator( } } + private fun openInBrowserPillClose() { + // Open in Browser X & Y Scaling Animation + animators += + ObjectAnimator.ofFloat(openInBrowserPill, SCALE_X, HALF_INITIAL_SCALE).apply { + duration = BODY_CLOSE_DURATION + } + + animators += + ObjectAnimator.ofFloat(openInBrowserPill, SCALE_Y, HALF_INITIAL_SCALE).apply { + duration = BODY_CLOSE_DURATION + } + + // Open in Browser Opacity Animation + animators += + ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 0f).apply { + duration = BODY_CLOSE_DURATION + } + + animators += + ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 0f).apply { + duration = BODY_CLOSE_DURATION + } + + // Upward Open in Browser y-translation Animation + val yStart: Float = -captionHeight / 2 + animators += + ObjectAnimator.ofFloat(openInBrowserPill, TRANSLATION_Y, yStart).apply { + duration = BODY_CLOSE_DURATION + } + } + /** * Runs the list of hide animators concurrently. * * @param after runs after animation finishes. */ - private fun runAnimations(after: Runnable? = null) { + private fun runAnimations(after: (() -> Unit)? = null) { runningAnimation?.apply { - // Remove all listeners, so that after runnable isn't triggered upon cancel. + // Remove all listeners, so that the after function isn't triggered upon cancel. removeAllListeners() // If an animation runs while running animation is triggered, gracefully cancel. cancel() @@ -396,7 +486,7 @@ class HandleMenuAnimator( playTogether(animators) animators.clear() doOnEnd { - after?.run() + after?.invoke() runningAnimation = null } start() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt index 18757ef6ff40..cf82bb4f9919 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt @@ -39,7 +39,7 @@ class HandleMenuImageButton( lateinit var taskInfo: RunningTaskInfo override fun onHoverEvent(motionEvent: MotionEvent): Boolean { - if (Flags.enableAdditionalWindowsAboveStatusBar() || taskInfo.isFreeform) { + if (Flags.enableHandleInputFix() || taskInfo.isFreeform) { return super.onHoverEvent(motionEvent) } else { return false diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt index 4f049015af99..2d97dc06cf89 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt @@ -31,8 +31,8 @@ import android.widget.ProgressBar import androidx.core.animation.doOnEnd import androidx.core.animation.doOnStart import androidx.core.content.ContextCompat -import com.android.window.flags.Flags import com.android.wm.shell.R +import android.window.flags.DesktopModeFlags private const val OPEN_MAXIMIZE_MENU_DELAY_ON_HOVER_MS = 350 private const val MAX_DRAWABLE_ALPHA = 255 @@ -108,7 +108,7 @@ class MaximizeButtonView( baseForegroundColor: Int? = null, rippleDrawable: RippleDrawable? = null ) { - if (Flags.enableThemedAppHeaders()) { + if (DesktopModeFlags.ENABLE_THEMED_APP_HEADERS.isTrue()) { requireNotNull(iconForegroundColor) { "Icon foreground color must be non-null" } requireNotNull(baseForegroundColor) { "Base foreground color must be non-null" } requireNotNull(rippleDrawable) { "Ripple drawable must be non-null" } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt index 0470367015ea..0cb219ae4b81 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt @@ -20,7 +20,6 @@ import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.animation.ValueAnimator import android.annotation.ColorInt -import android.annotation.IdRes import android.app.ActivityManager.RunningTaskInfo import android.content.Context import android.content.res.ColorStateList @@ -28,6 +27,7 @@ import android.content.res.Resources import android.graphics.Paint import android.graphics.PixelFormat import android.graphics.PointF +import android.graphics.Rect import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.graphics.drawable.LayerDrawable @@ -37,18 +37,21 @@ import android.graphics.drawable.shapes.RoundRectShape import android.util.StateSet import android.view.LayoutInflater import android.view.MotionEvent +import android.view.MotionEvent.ACTION_HOVER_ENTER +import android.view.MotionEvent.ACTION_HOVER_EXIT +import android.view.MotionEvent.ACTION_HOVER_MOVE +import android.view.MotionEvent.ACTION_OUTSIDE import android.view.SurfaceControl import android.view.SurfaceControl.Transaction import android.view.SurfaceControlViewHost import android.view.View -import android.view.View.OnClickListener -import android.view.View.OnGenericMotionListener -import android.view.View.OnTouchListener import android.view.View.SCALE_Y import android.view.View.TRANSLATION_Y import android.view.View.TRANSLATION_Z +import android.view.ViewGroup import android.view.WindowManager import android.view.WindowlessWindowManager +import android.view.accessibility.AccessibilityEvent import android.widget.Button import android.widget.TextView import android.window.TaskConstants @@ -57,9 +60,10 @@ import androidx.compose.ui.graphics.toArgb import androidx.core.animation.addListener import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer -import com.android.wm.shell.animation.Interpolators.EMPHASIZED_DECELERATE import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.shared.animation.Interpolators.EMPHASIZED_DECELERATE +import com.android.wm.shell.shared.animation.Interpolators.FAST_OUT_LINEAR_IN import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer import com.android.wm.shell.windowdecor.common.DecorThemeUtil import com.android.wm.shell.windowdecor.common.OPACITY_12 @@ -67,7 +71,6 @@ import com.android.wm.shell.windowdecor.common.OPACITY_40 import com.android.wm.shell.windowdecor.common.withAlpha import java.util.function.Supplier - /** * Menu that appears when user long clicks the maximize button. Gives the user the option to * maximize the task or snap the task to the right or left half of the screen. @@ -77,9 +80,6 @@ class MaximizeMenu( private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer, private val displayController: DisplayController, private val taskInfo: RunningTaskInfo, - private val onClickListener: OnClickListener, - private val onGenericMotionListener: OnGenericMotionListener, - private val onTouchListener: OnTouchListener, private val decorWindowContext: Context, private val menuPosition: PointF, private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() } @@ -102,22 +102,52 @@ class MaximizeMenu( } /** Creates and shows the maximize window. */ - fun show() { + fun show( + onMaximizeOrRestoreClickListener: () -> Unit, + onLeftSnapClickListener: () -> Unit, + onRightSnapClickListener: () -> Unit, + onHoverListener: (Boolean) -> Unit, + onOutsideTouchListener: () -> Unit, + ) { if (maximizeMenu != null) return - createMaximizeMenu() - maximizeMenuView?.animateOpenMenu() + createMaximizeMenu( + onMaximizeClickListener = onMaximizeOrRestoreClickListener, + onLeftSnapClickListener = onLeftSnapClickListener, + onRightSnapClickListener = onRightSnapClickListener, + onHoverListener = onHoverListener, + onOutsideTouchListener = onOutsideTouchListener + ) + maximizeMenuView?.let { view -> + view.animateOpenMenu(onEnd = { + view.requestAccessibilityFocus() + }) + } } /** Closes the maximize window and releases its view. */ - fun close() { - maximizeMenuView?.cancelAnimation() - maximizeMenu?.releaseView() + fun close(onEnd: () -> Unit) { + val view = maximizeMenuView + val menu = maximizeMenu + if (view == null) { + menu?.releaseView() + } else { + view.animateCloseMenu(onEnd = { + menu?.releaseView() + onEnd.invoke() + }) + } maximizeMenu = null maximizeMenuView = null } /** Create a maximize menu that is attached to the display area. */ - private fun createMaximizeMenu() { + private fun createMaximizeMenu( + onMaximizeClickListener: () -> Unit, + onLeftSnapClickListener: () -> Unit, + onRightSnapClickListener: () -> Unit, + onHoverListener: (Boolean) -> Unit, + onOutsideTouchListener: () -> Unit + ) { val t = transactionSupplier.get() val builder = SurfaceControl.Builder() rootTdaOrganizer.attachToDisplayArea(taskInfo.displayId, builder) @@ -129,7 +159,9 @@ class MaximizeMenu( menuWidth, menuHeight, WindowManager.LayoutParams.TYPE_APPLICATION, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH + or WindowManager.LayoutParams.FLAG_SPLIT_TOUCH, PixelFormat.TRANSPARENT ) lp.title = "Maximize Menu for Task=" + taskInfo.taskId @@ -146,11 +178,13 @@ class MaximizeMenu( context = decorWindowContext, menuHeight = menuHeight, menuPadding = menuPadding, - onClickListener = onClickListener, - onTouchListener = onTouchListener, - onGenericMotionListener = onGenericMotionListener, ).also { menuView -> menuView.bind(taskInfo) + menuView.onMaximizeClickListener = onMaximizeClickListener + menuView.onLeftSnapClickListener = onLeftSnapClickListener + menuView.onRightSnapClickListener = onRightSnapClickListener + menuView.onMenuHoverListener = onHoverListener + menuView.onOutsideTouchListener = onOutsideTouchListener viewHost.setView(menuView.rootView, lp) } @@ -198,56 +232,6 @@ class MaximizeMenu( } /** - * Called when a [MotionEvent.ACTION_HOVER_ENTER] is triggered on any of the menu's views. - * - * TODO(b/346440693): this is only needed for the left/right snap options that don't support - * selector states to manage its hover state. Look into whether that can be added to avoid - * manually tracking hover enter/exit motion events. Also because those button colors/states - * aren't updating correctly for pressed, focused and selected states. - * See also [onMaximizeMenuHoverMove] and [onMaximizeMenuHoverExit]. - */ - fun onMaximizeMenuHoverEnter(viewId: Int, ev: MotionEvent) { - setSnapButtonsColorOnHover(viewId, ev) - } - - /** Called when a [MotionEvent.ACTION_HOVER_MOVE] is triggered on any of the menu's views. */ - fun onMaximizeMenuHoverMove(viewId: Int, ev: MotionEvent) { - setSnapButtonsColorOnHover(viewId, ev) - } - - /** Called when a [MotionEvent.ACTION_HOVER_EXIT] is triggered on any of the menu's views. */ - fun onMaximizeMenuHoverExit(id: Int, ev: MotionEvent) { - val snapOptionsWidth = maximizeMenuView?.snapOptionsWidth ?: return - val snapOptionsHeight = maximizeMenuView?.snapOptionsHeight ?: return - val inSnapMenuBounds = ev.x >= 0 && ev.x <= snapOptionsWidth && - ev.y >= 0 && ev.y <= snapOptionsHeight - - if (id == R.id.maximize_menu_snap_menu_layout && !inSnapMenuBounds) { - // After exiting the snap menu layout area, checks to see that user is not still - // hovering within the snap menu layout bounds which would indicate that the user is - // hovering over a snap button within the snap menu layout rather than having exited. - maximizeMenuView?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.NONE) - } - } - - private fun setSnapButtonsColorOnHover(viewId: Int, ev: MotionEvent) { - val snapOptionsWidth = maximizeMenuView?.snapOptionsWidth ?: return - val snapMenuCenter = snapOptionsWidth / 2 - when { - viewId == R.id.maximize_menu_snap_left_button || - (viewId == R.id.maximize_menu_snap_menu_layout && ev.x <= snapMenuCenter) -> { - maximizeMenuView - ?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.LEFT) - } - viewId == R.id.maximize_menu_snap_right_button || - (viewId == R.id.maximize_menu_snap_menu_layout && ev.x > snapMenuCenter) -> { - maximizeMenuView - ?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.RIGHT) - } - } - } - - /** * The view within the Maximize Menu, presents maximize, restore and snap-to-side options for * resizing a Task. */ @@ -255,12 +239,11 @@ class MaximizeMenu( context: Context, private val menuHeight: Int, private val menuPadding: Int, - onClickListener: OnClickListener, - onTouchListener: OnTouchListener, - onGenericMotionListener: OnGenericMotionListener, ) { - val rootView: View = LayoutInflater.from(context) - .inflate(R.layout.desktop_mode_window_decor_maximize_menu, null /* root */) + val rootView = LayoutInflater.from(context) + .inflate(R.layout.desktop_mode_window_decor_maximize_menu, null /* root */) as ViewGroup + private val container = requireViewById(R.id.container) + private val overlay = requireViewById(R.id.maximize_menu_overlay) private val maximizeText = requireViewById(R.id.maximize_menu_maximize_window_text) as TextView private val maximizeButton = @@ -285,30 +268,72 @@ class MaximizeMenu( private val fillRadius = context.resources .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_radius) - private val openMenuAnimatorSet = AnimatorSet() + private val hoverTempRect = Rect() + private var menuAnimatorSet: AnimatorSet? = null private lateinit var taskInfo: RunningTaskInfo private lateinit var style: MenuStyle - /** The width of the snap menu option view, including both left and right snaps. */ - val snapOptionsWidth: Int - get() = snapButtonsLayout.width - /** The height of the snap menu option view, including both left and right snaps .*/ - val snapOptionsHeight: Int - get() = snapButtonsLayout.height + /** Invoked when the maximize or restore option is clicked. */ + var onMaximizeClickListener: (() -> Unit)? = null + /** Invoked when the left snap option is clicked. */ + var onLeftSnapClickListener: (() -> Unit)? = null + /** Invoked when the right snap option is clicked. */ + var onRightSnapClickListener: (() -> Unit)? = null + /** Invoked whenever the hover state of the menu changes. */ + var onMenuHoverListener: ((Boolean) -> Unit)? = null + /** Invoked whenever a click occurs outside the menu */ + var onOutsideTouchListener: (() -> Unit)? = null init { - // TODO(b/346441962): encapsulate menu hover enter/exit logic inside this class and - // expose only what is actually relevant to outside classes so that specific checks - // against resource IDs aren't needed outside this class. - rootView.setOnGenericMotionListener(onGenericMotionListener) - rootView.setOnTouchListener(onTouchListener) - maximizeButton.setOnClickListener(onClickListener) - maximizeButton.setOnGenericMotionListener(onGenericMotionListener) - snapRightButton.setOnClickListener(onClickListener) - snapRightButton.setOnGenericMotionListener(onGenericMotionListener) - snapLeftButton.setOnClickListener(onClickListener) - snapLeftButton.setOnGenericMotionListener(onGenericMotionListener) - snapButtonsLayout.setOnGenericMotionListener(onGenericMotionListener) + overlay.setOnHoverListener { _, event -> + // The overlay covers the entire menu, so it's a convenient way to monitor whether + // the menu is hovered as a whole or not. + when (event.action) { + ACTION_HOVER_ENTER -> onMenuHoverListener?.invoke(true) + ACTION_HOVER_EXIT -> onMenuHoverListener?.invoke(false) + } + + // Also check if the hover falls within the snap options layout, to manually + // set the left/right state based on the event's position. + // TODO(b/346440693): this manual hover tracking is needed for left/right snap + // because its view/background(s) don't support selector states. Look into whether + // that can be added to avoid manual tracking. Also because these button + // colors/state logic is only being applied on hover events, but there's pressed, + // focused and selected states that should be responsive too. + val snapLayoutBoundsRelToOverlay = hoverTempRect.also { rect -> + snapButtonsLayout.getDrawingRect(rect) + rootView.offsetDescendantRectToMyCoords(snapButtonsLayout, rect) + } + if (event.action == ACTION_HOVER_ENTER || event.action == ACTION_HOVER_MOVE) { + if (snapLayoutBoundsRelToOverlay.contains(event.x.toInt(), event.y.toInt())) { + // Hover is inside the snap layout, anything left of center is the left + // snap, and anything right of center is right snap. + val layoutCenter = snapLayoutBoundsRelToOverlay.centerX() + if (event.x < layoutCenter) { + updateSplitSnapSelection(SnapToHalfSelection.LEFT) + } else { + updateSplitSnapSelection(SnapToHalfSelection.RIGHT) + } + } else { + // Any other hover is outside the snap layout, so neither is selected. + updateSplitSnapSelection(SnapToHalfSelection.NONE) + } + } + + // Don't consume the event to allow child views to receive the event too. + return@setOnHoverListener false + } + + maximizeButton.setOnClickListener { onMaximizeClickListener?.invoke() } + snapRightButton.setOnClickListener { onRightSnapClickListener?.invoke() } + snapLeftButton.setOnClickListener { onLeftSnapClickListener?.invoke() } + rootView.setOnTouchListener { _, event -> + if (event.actionMasked == ACTION_OUTSIDE) { + onOutsideTouchListener?.invoke() + return@setOnTouchListener false + } + true + } // To prevent aliasing. maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) @@ -332,18 +357,19 @@ class MaximizeMenu( } /** Animate the opening of the menu */ - fun animateOpenMenu() { + fun animateOpenMenu(onEnd: () -> Unit) { maximizeButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) maximizeText.setLayerType(View.LAYER_TYPE_HARDWARE, null) - openMenuAnimatorSet.playTogether( + menuAnimatorSet = AnimatorSet() + menuAnimatorSet?.playTogether( ObjectAnimator.ofFloat(rootView, SCALE_Y, STARTING_MENU_HEIGHT_SCALE, 1f) .apply { - duration = MENU_HEIGHT_ANIMATION_DURATION_MS + duration = OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS interpolator = EMPHASIZED_DECELERATE }, ValueAnimator.ofFloat(STARTING_MENU_HEIGHT_SCALE, 1f) .apply { - duration = MENU_HEIGHT_ANIMATION_DURATION_MS + duration = OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS interpolator = EMPHASIZED_DECELERATE addUpdateListener { // Animate padding so that controls stay pinned to the bottom of @@ -351,12 +377,12 @@ class MaximizeMenu( val value = animatedValue as Float val topPadding = menuPadding - ((1 - value) * menuHeight).toInt() - rootView.setPadding(menuPadding, topPadding, + container.setPadding(menuPadding, topPadding, menuPadding, menuPadding) } }, ValueAnimator.ofFloat(1 / STARTING_MENU_HEIGHT_SCALE, 1f).apply { - duration = MENU_HEIGHT_ANIMATION_DURATION_MS + duration = OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS interpolator = EMPHASIZED_DECELERATE addUpdateListener { // Scale up the children of the maximize menu so that the menu @@ -370,7 +396,7 @@ class MaximizeMenu( }, ObjectAnimator.ofFloat(rootView, TRANSLATION_Y, (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight, 0f).apply { - duration = MENU_HEIGHT_ANIMATION_DURATION_MS + duration = OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS interpolator = EMPHASIZED_DECELERATE }, ObjectAnimator.ofInt(rootView.background, "alpha", @@ -380,7 +406,7 @@ class MaximizeMenu( ValueAnimator.ofFloat(0f, 1f) .apply { duration = ALPHA_ANIMATION_DURATION_MS - startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS + startDelay = CONTROLS_ALPHA_OPEN_MENU_ANIMATION_DELAY_MS addUpdateListener { val value = animatedValue as Float maximizeButton.alpha = value @@ -392,25 +418,109 @@ class MaximizeMenu( ObjectAnimator.ofFloat(rootView, TRANSLATION_Z, MENU_Z_TRANSLATION) .apply { duration = ELEVATION_ANIMATION_DURATION_MS - startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS + startDelay = CONTROLS_ALPHA_OPEN_MENU_ANIMATION_DELAY_MS } ) - openMenuAnimatorSet.addListener( + menuAnimatorSet?.addListener( onEnd = { maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + onEnd.invoke() } ) - openMenuAnimatorSet.start() + menuAnimatorSet?.start() } - /** Cancel the open menu animation. */ - fun cancelAnimation() { - openMenuAnimatorSet.cancel() + /** Animate the closing of the menu */ + fun animateCloseMenu(onEnd: (() -> Unit)) { + maximizeButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) + maximizeText.setLayerType(View.LAYER_TYPE_HARDWARE, null) + cancelAnimation() + menuAnimatorSet = AnimatorSet() + menuAnimatorSet?.playTogether( + ObjectAnimator.ofFloat(rootView, SCALE_Y, 1f, STARTING_MENU_HEIGHT_SCALE) + .apply { + duration = CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS + interpolator = FAST_OUT_LINEAR_IN + }, + ValueAnimator.ofFloat(1f, STARTING_MENU_HEIGHT_SCALE) + .apply { + duration = CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS + interpolator = FAST_OUT_LINEAR_IN + addUpdateListener { + // Animate padding so that controls stay pinned to the bottom of + // the menu. + val value = animatedValue as Float + val topPadding = menuPadding - + ((1 - value) * menuHeight).toInt() + container.setPadding(menuPadding, topPadding, + menuPadding, menuPadding) + } + }, + ValueAnimator.ofFloat(1f, 1 / STARTING_MENU_HEIGHT_SCALE).apply { + duration = CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS + interpolator = FAST_OUT_LINEAR_IN + addUpdateListener { + // Scale up the children of the maximize menu so that the menu + // scale is cancelled out and only the background is scaled. + val value = animatedValue as Float + maximizeButton.scaleY = value + snapButtonsLayout.scaleY = value + maximizeText.scaleY = value + snapWindowText.scaleY = value + } + }, + ObjectAnimator.ofFloat(rootView, TRANSLATION_Y, + 0f, (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight).apply { + duration = CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS + interpolator = FAST_OUT_LINEAR_IN + }, + ObjectAnimator.ofInt(rootView.background, "alpha", + MAX_DRAWABLE_ALPHA_VALUE, 0).apply { + startDelay = CONTAINER_ALPHA_CLOSE_MENU_ANIMATION_DELAY_MS + duration = ALPHA_ANIMATION_DURATION_MS + }, + ValueAnimator.ofFloat(1f, 0f) + .apply { + duration = ALPHA_ANIMATION_DURATION_MS + addUpdateListener { + val value = animatedValue as Float + maximizeButton.alpha = value + snapButtonsLayout.alpha = value + maximizeText.alpha = value + snapWindowText.alpha = value + } + }, + ObjectAnimator.ofFloat(rootView, TRANSLATION_Z, MENU_Z_TRANSLATION, 0f) + .apply { + duration = ELEVATION_ANIMATION_DURATION_MS + } + ) + menuAnimatorSet?.addListener( + onEnd = { + maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + onEnd?.invoke() + } + ) + menuAnimatorSet?.start() + } + + /** Request that the accessibility service focus on the menu. */ + fun requestAccessibilityFocus() { + // Focus the first button in the menu by default. + maximizeButton.post { + maximizeButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) + } + } + + /** Cancel the menu animation. */ + private fun cancelAnimation() { + menuAnimatorSet?.cancel() } /** Update the view state to a new snap to half selection. */ - fun updateSplitSnapSelection(selection: SnapToHalfSelection) { + private fun updateSplitSnapSelection(selection: SnapToHalfSelection) { when (selection) { SnapToHalfSelection.NONE -> deactivateSnapOptions() SnapToHalfSelection.LEFT -> activateSnapOption(activateLeft = true) @@ -634,17 +744,47 @@ class MaximizeMenu( private const val ALPHA_ANIMATION_DURATION_MS = 50L private const val MAX_DRAWABLE_ALPHA_VALUE = 255 private const val STARTING_MENU_HEIGHT_SCALE = 0.8f - private const val MENU_HEIGHT_ANIMATION_DURATION_MS = 300L + private const val OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS = 300L + private const val CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS = 200L private const val ELEVATION_ANIMATION_DURATION_MS = 50L - private const val CONTROLS_ALPHA_ANIMATION_DELAY_MS = 33L + private const val CONTROLS_ALPHA_OPEN_MENU_ANIMATION_DELAY_MS = 33L + private const val CONTAINER_ALPHA_CLOSE_MENU_ANIMATION_DELAY_MS = 33L private const val MENU_Z_TRANSLATION = 1f - fun isMaximizeMenuView(@IdRes viewId: Int): Boolean { - return viewId == R.id.maximize_menu || - viewId == R.id.maximize_menu_maximize_button || - viewId == R.id.maximize_menu_snap_left_button || - viewId == R.id.maximize_menu_snap_right_button || - viewId == R.id.maximize_menu_snap_menu_layout || - viewId == R.id.maximize_menu_snap_menu_layout - } + } +} + +/** A factory interface to create a [MaximizeMenu]. */ +interface MaximizeMenuFactory { + fun create( + syncQueue: SyncTransactionQueue, + rootTdaOrganizer: RootTaskDisplayAreaOrganizer, + displayController: DisplayController, + taskInfo: RunningTaskInfo, + decorWindowContext: Context, + menuPosition: PointF, + transactionSupplier: Supplier<Transaction> + ): MaximizeMenu +} + +/** A [MaximizeMenuFactory] implementation that creates a [MaximizeMenu]. */ +object DefaultMaximizeMenuFactory : MaximizeMenuFactory { + override fun create( + syncQueue: SyncTransactionQueue, + rootTdaOrganizer: RootTaskDisplayAreaOrganizer, + displayController: DisplayController, + taskInfo: RunningTaskInfo, + decorWindowContext: Context, + menuPosition: PointF, + transactionSupplier: Supplier<Transaction> + ): MaximizeMenu { + return MaximizeMenu( + syncQueue, + rootTdaOrganizer, + displayController, + taskInfo, + decorWindowContext, + menuPosition, + transactionSupplier + ) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt index 974166700203..70c0b54462e3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt @@ -7,6 +7,7 @@ import android.graphics.PointF import android.graphics.Rect import android.view.MotionEvent import android.view.SurfaceControl +import android.view.VelocityTracker import com.android.wm.shell.R /** @@ -34,6 +35,7 @@ class MoveToDesktopAnimator @JvmOverloads constructor( val scale: Float get() = dragToDesktopAnimator.animatedValue as Float private val mostRecentInput = PointF() + private val velocityTracker = VelocityTracker.obtain() private val dragToDesktopAnimator: ValueAnimator = ValueAnimator.ofFloat(1f, DRAG_FREEFORM_SCALE) .setDuration(ANIMATION_DURATION.toLong()) @@ -90,6 +92,7 @@ class MoveToDesktopAnimator @JvmOverloads constructor( if (!allowSurfaceChangesOnMove || dragToDesktopAnimator.isRunning) { return } + velocityTracker.addMovement(ev) setTaskPosition(ev.rawX, ev.rawY) val t = transactionFactory() t.setPosition(taskSurface, position.x, position.y) @@ -109,6 +112,15 @@ class MoveToDesktopAnimator @JvmOverloads constructor( * Cancels the animation, intended to be used when another animator will take over. */ fun cancelAnimator() { + velocityTracker.clear() dragToDesktopAnimator.cancel() } + + /** + * Computes the current velocity per second based on the points that have been collected. + */ + fun computeCurrentVelocity(): PointF { + velocityTracker.computeCurrentVelocity(/* units = */ 1000) + return PointF(velocityTracker.xVelocity, velocityTracker.yVelocity) + } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OnTaskRepositionAnimationListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OnTaskRepositionAnimationListener.kt new file mode 100644 index 000000000000..214a6fa0b200 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OnTaskRepositionAnimationListener.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 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.windowdecor + +/** + * Listener that allows notifies when an animation that is repositioning a task is starting + * and finishing the animation. + */ +interface OnTaskRepositionAnimationListener { + /** + * Notifies that an animation is about to be started. + */ + fun onAnimationStart(taskId: Int) + + /** + * Notifies that an animation is about to be finished. + */ + fun onAnimationEnd(taskId: Int) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt index cd2dac806a7f..fb81ed4169ff 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt @@ -30,8 +30,8 @@ import android.view.Display import android.view.LayoutInflater import android.view.SurfaceControl import android.view.SurfaceControlViewHost -import android.view.SurfaceSession import android.view.WindowManager +import android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL import android.view.WindowlessWindowManager import android.widget.ImageView import android.window.TaskConstants @@ -65,7 +65,6 @@ class ResizeVeil @JvmOverloads constructor( private val lightColors = dynamicLightColorScheme(context) private val darkColors = dynamicDarkColorScheme(context) - private val surfaceSession = SurfaceSession() private lateinit var iconView: ImageView private var iconSize = 0 @@ -125,7 +124,7 @@ class ResizeVeil @JvmOverloads constructor( .setCallsite("ResizeVeil#setupResizeVeil") .build() backgroundSurface = surfaceControlBuilderFactory - .create("Resize veil background of Task=" + taskInfo.taskId, surfaceSession) + .create("Resize veil background of Task=" + taskInfo.taskId) .setColorLayer() .setHidden(true) .setParent(veilSurface) @@ -151,6 +150,7 @@ class ResizeVeil @JvmOverloads constructor( WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT) lp.title = "Resize veil icon window of Task=" + taskInfo.taskId + lp.inputFeatures = INPUT_FEATURE_NO_INPUT_CHANNEL lp.setTrustedOverlay() val wwm = WindowlessWindowManager(taskInfo.configuration, iconSurface, null /* hostInputToken */) @@ -397,10 +397,6 @@ class ResizeVeil @JvmOverloads constructor( fun create(name: String): SurfaceControl.Builder { return SurfaceControl.Builder().setName(name) } - - fun create(name: String, surfaceSession: SurfaceSession): SurfaceControl.Builder { - return SurfaceControl.Builder(surfaceSession).setName(name) - } } companion object { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java index 40421b599889..d7ea0c3a8e62 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java @@ -19,7 +19,7 @@ package com.android.wm.shell.windowdecor; /** * Holds the state of a drag resize. */ -interface TaskDragResizer { +public interface TaskDragResizer { /** * Returns true if task is currently being resized or animating the final transition after diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java index ad238c35dd83..61b93932013c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java @@ -23,6 +23,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.hardware.input.InputManager; +import android.os.IBinder; import android.os.SystemClock; import android.util.Log; import android.view.InputDevice; @@ -84,13 +85,17 @@ class TaskOperations { } } - void minimizeTask(WindowContainerToken taskToken) { - WindowContainerTransaction wct = new WindowContainerTransaction(); + IBinder minimizeTask(WindowContainerToken taskToken) { + return minimizeTask(taskToken, new WindowContainerTransaction()); + } + + IBinder minimizeTask(WindowContainerToken taskToken, WindowContainerTransaction wct) { wct.reorder(taskToken, false); if (Transitions.ENABLE_SHELL_TRANSITIONS) { - mTransitionStarter.startMinimizedModeTransition(wct); + return mTransitionStarter.startMinimizedModeTransition(wct); } else { mSyncQueue.queue(wct); + return null; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.kt new file mode 100644 index 000000000000..96c43da0cdf2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 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.windowdecor + +/** + * Interface for TaskPositioner. + */ +interface TaskPositioner : DragPositioningCallback, TaskDragResizer diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java index 5fce5d228d71..6eb5cca9ad1a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java @@ -18,10 +18,16 @@ package com.android.wm.shell.windowdecor; import static android.view.WindowManager.TRANSIT_CHANGE; +import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW; +import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_RESIZE_WINDOW; + import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; +import android.view.Choreographer; import android.view.Surface; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -31,8 +37,10 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.transition.Transitions; import java.util.function.Supplier; @@ -43,8 +51,7 @@ import java.util.function.Supplier; * If the drag is resizing the task, we resize the veil instead. * If the drag is repositioning, we update in the typical manner. */ -public class VeiledResizeTaskPositioner implements DragPositioningCallback, - TaskDragResizer, Transitions.TransitionHandler { +public class VeiledResizeTaskPositioner implements TaskPositioner, Transitions.TransitionHandler { private DesktopModeWindowDecoration mDesktopWindowDecoration; private ShellTaskOrganizer mTaskOrganizer; @@ -56,30 +63,37 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, private final PointF mRepositionStartPoint = new PointF(); private final Rect mRepositionTaskBounds = new Rect(); private final Supplier<SurfaceControl.Transaction> mTransactionSupplier; + private final InteractionJankMonitor mInteractionJankMonitor; private int mCtrlType; private boolean mIsResizingOrAnimatingResize; @Surface.Rotation private int mRotation; + @ShellMainThread + private final Handler mHandler; public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, DesktopModeWindowDecoration windowDecoration, DisplayController displayController, DragPositioningCallbackUtility.DragStartListener dragStartListener, - Transitions transitions) { + Transitions transitions, InteractionJankMonitor interactionJankMonitor, + @ShellMainThread Handler handler) { this(taskOrganizer, windowDecoration, displayController, dragStartListener, - SurfaceControl.Transaction::new, transitions); + SurfaceControl.Transaction::new, transitions, interactionJankMonitor, handler); } public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, DesktopModeWindowDecoration windowDecoration, DisplayController displayController, DragPositioningCallbackUtility.DragStartListener dragStartListener, - Supplier<SurfaceControl.Transaction> supplier, Transitions transitions) { + Supplier<SurfaceControl.Transaction> supplier, Transitions transitions, + InteractionJankMonitor interactionJankMonitor, @ShellMainThread Handler handler) { mDesktopWindowDecoration = windowDecoration; mTaskOrganizer = taskOrganizer; mDisplayController = displayController; mDragStartListener = dragStartListener; mTransactionSupplier = supplier; mTransitions = transitions; + mInteractionJankMonitor = interactionJankMonitor; + mHandler = handler; } @Override @@ -89,6 +103,9 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, mDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds()); mRepositionStartPoint.set(x, y); if (isResizing()) { + // Capture CUJ for re-sizing window in DW mode. + mInteractionJankMonitor.begin(mDesktopWindowDecoration.mTaskSurface, + mDesktopWindowDecoration.mContext, mHandler, CUJ_DESKTOP_MODE_RESIZE_WINDOW); if (!mDesktopWindowDecoration.mTaskInfo.isFocused) { WindowContainerTransaction wct = new WindowContainerTransaction(); wct.reorder(mDesktopWindowDecoration.mTaskInfo.token, true); @@ -109,6 +126,11 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, @Override public Rect onDragPositioningMove(float x, float y) { + if (Looper.myLooper() != mHandler.getLooper()) { + // This method must run on the shell main thread to use the correct Choreographer + // instance below. + throw new IllegalStateException("This method must run on the shell main thread."); + } PointF delta = DragPositioningCallbackUtility.calculateDelta(x, y, mRepositionStartPoint); if (isResizing() && DragPositioningCallbackUtility.changeBounds(mCtrlType, mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds, delta, @@ -120,9 +142,13 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, mDesktopWindowDecoration.updateResizeVeil(mRepositionTaskBounds); } } else if (mCtrlType == CTRL_TYPE_UNDEFINED) { + // Begin window drag CUJ instrumentation only when drag position moves. + mInteractionJankMonitor.begin(mDesktopWindowDecoration.mTaskSurface, + mDesktopWindowDecoration.mContext, mHandler, CUJ_DESKTOP_MODE_DRAG_WINDOW); final SurfaceControl.Transaction t = mTransactionSupplier.get(); DragPositioningCallbackUtility.setPositionOnDrag(mDesktopWindowDecoration, mRepositionTaskBounds, mTaskBoundsAtDragStart, mRepositionStartPoint, t, x, y); + t.setFrameTimeline(Choreographer.getInstance().getVsyncId()); t.apply(); } return new Rect(mRepositionTaskBounds); @@ -146,12 +172,11 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, // won't be called. resetVeilIfVisible(); } + mInteractionJankMonitor.end(CUJ_DESKTOP_MODE_RESIZE_WINDOW); } else { - final WindowContainerTransaction wct = new WindowContainerTransaction(); DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds, mTaskBoundsAtDragStart, mRepositionStartPoint, x, y); - wct.setBounds(mDesktopWindowDecoration.mTaskInfo.token, mRepositionTaskBounds); - mTransitions.startTransition(TRANSIT_CHANGE, wct, this); + mInteractionJankMonitor.end(CUJ_DESKTOP_MODE_DRAG_WINDOW); } mCtrlType = CTRL_TYPE_UNDEFINED; @@ -192,6 +217,7 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, mCtrlType = CTRL_TYPE_UNDEFINED; finishCallback.onTransitionFinished(null); mIsResizingOrAnimatingResize = false; + mInteractionJankMonitor.end(CUJ_DESKTOP_MODE_DRAG_WINDOW); return true; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 216990c35247..369484558325 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -22,6 +22,9 @@ import static android.content.res.Configuration.DENSITY_DPI_UNDEFINED; import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsets.Type.mandatorySystemGestures; import static android.view.WindowInsets.Type.statusBars; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; +import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import android.annotation.NonNull; @@ -55,9 +58,12 @@ import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.shared.DesktopModeStatus; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams.OccludingCaptionElement; import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer; +import com.android.wm.shell.windowdecor.extension.InsetsStateKt; +import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHost; +import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier; import java.util.ArrayList; import java.util.Arrays; @@ -105,12 +111,14 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> * System-wide context. Only used to create context with overridden configurations. */ final Context mContext; - final DisplayController mDisplayController; + final @NonNull Context mUserContext; + final @NonNull DisplayController mDisplayController; final ShellTaskOrganizer mTaskOrganizer; final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier; final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier; final Supplier<WindowContainerTransaction> mWindowContainerTransactionSupplier; final SurfaceControlViewHostFactory mSurfaceControlViewHostFactory; + @NonNull private final WindowDecorViewHostSupplier mWindowDecorViewHostSupplier; private final DisplayController.OnDisplaysChangedListener mOnDisplaysChangedListener = new DisplayController.OnDisplaysChangedListener() { @Override @@ -132,12 +140,13 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> Context mDecorWindowContext; SurfaceControl mDecorationContainerSurface; - SurfaceControl mCaptionContainerSurface; - private WindowlessWindowManager mCaptionWindowManager; - private SurfaceControlViewHost mViewHost; + private WindowDecorViewHost mDecorViewHost; private Configuration mWindowDecorConfig; TaskDragResizer mTaskDragResizer; - private boolean mIsCaptionVisible; + boolean mIsCaptionVisible; + + private boolean mIsStatusBarVisible; + private boolean mIsKeyguardVisibleAndOccluded; /** The most recent set of insets applied to this window decoration. */ private WindowDecorationInsets mWindowDecorationInsets; @@ -146,19 +155,23 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> WindowDecoration( Context context, + @NonNull Context userContext, DisplayController displayController, ShellTaskOrganizer taskOrganizer, RunningTaskInfo taskInfo, - SurfaceControl taskSurface) { - this(context, displayController, taskOrganizer, taskInfo, taskSurface, + SurfaceControl taskSurface, + @NonNull WindowDecorViewHostSupplier windowDecorViewHostSupplier) { + this(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface, SurfaceControl.Builder::new, SurfaceControl.Transaction::new, WindowContainerTransaction::new, SurfaceControl::new, - new SurfaceControlViewHostFactory() {}); + new SurfaceControlViewHostFactory() {}, + windowDecorViewHostSupplier); } WindowDecoration( Context context, - DisplayController displayController, + @NonNull Context userContext, + @NonNull DisplayController displayController, ShellTaskOrganizer taskOrganizer, RunningTaskInfo taskInfo, @NonNull SurfaceControl taskSurface, @@ -166,8 +179,10 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, Supplier<WindowContainerTransaction> windowContainerTransactionSupplier, Supplier<SurfaceControl> surfaceControlSupplier, - SurfaceControlViewHostFactory surfaceControlViewHostFactory) { + SurfaceControlViewHostFactory surfaceControlViewHostFactory, + @NonNull WindowDecorViewHostSupplier windowDecorViewHostSupplier) { mContext = context; + mUserContext = userContext; mDisplayController = displayController; mTaskOrganizer = taskOrganizer; mTaskInfo = taskInfo; @@ -176,8 +191,11 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier; mWindowContainerTransactionSupplier = windowContainerTransactionSupplier; mSurfaceControlViewHostFactory = surfaceControlViewHostFactory; - + mWindowDecorViewHostSupplier = windowDecorViewHostSupplier; mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId); + final InsetsState insetsState = mDisplayController.getInsetsState(mTaskInfo.displayId); + mIsStatusBarVisible = insetsState != null + && InsetsStateKt.isVisible(insetsState, statusBars()); } /** @@ -199,15 +217,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> void relayout(RelayoutParams params, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, WindowContainerTransaction wct, T rootView, RelayoutResult<T> outResult) { - updateViewsAndSurfaces(params, startT, finishT, wct, rootView, outResult); - if (outResult.mRootView != null) { - updateViewHost(params, startT, outResult); - } - } - - protected void updateViewsAndSurfaces(RelayoutParams params, - SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, - WindowContainerTransaction wct, T rootView, RelayoutResult<T> outResult) { + Trace.beginSection("WindowDecoration#relayout"); outResult.reset(); if (params.mRunningTaskInfo != null) { mTaskInfo = params.mRunningTaskInfo; @@ -218,32 +228,50 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> if (!mTaskInfo.isVisible) { releaseViews(wct); finishT.hide(mTaskSurface); + Trace.endSection(); // WindowDecoration#relayout return; } - + Trace.beginSection("WindowDecoration#relayout-inflateIfNeeded"); inflateIfNeeded(params, wct, rootView, oldLayoutResId, outResult); - if (outResult.mRootView == null) { - // Didn't manage to create a root view, early out. + Trace.endSection(); + final boolean hasCaptionView = outResult.mRootView != null; + if (!hasCaptionView) { + Trace.endSection(); // WindowDecoration#relayout return; } - rootView = null; // Clear it just in case we use it accidentally - updateCaptionVisibility(outResult.mRootView, mTaskInfo.displayId); + Trace.beginSection("WindowDecoration#relayout-updateCaptionVisibility"); + updateCaptionVisibility(outResult.mRootView); + Trace.endSection(); final Rect taskBounds = mTaskInfo.getConfiguration().windowConfiguration.getBounds(); outResult.mWidth = taskBounds.width(); outResult.mHeight = taskBounds.height(); outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused); final Resources resources = mDecorWindowContext.getResources(); - outResult.mCaptionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId); + outResult.mCaptionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId) + + params.mCaptionTopPadding; outResult.mCaptionWidth = params.mCaptionWidthId != Resources.ID_NULL ? loadDimensionPixelSize(resources, params.mCaptionWidthId) : taskBounds.width(); outResult.mCaptionX = (outResult.mWidth - outResult.mCaptionWidth) / 2; + Trace.beginSection("WindowDecoration#relayout-acquire"); + if (mDecorViewHost == null) { + mDecorViewHost = mWindowDecorViewHostSupplier.acquire(mDecorWindowContext, mDisplay); + } + Trace.endSection(); + + final SurfaceControl captionSurface = mDecorViewHost.getSurfaceControl(); + Trace.beginSection("WindowDecoration#relayout-updateSurfacesAndInsets"); updateDecorationContainerSurface(startT, outResult); - updateCaptionContainerSurface(startT, outResult); + updateCaptionContainerSurface(captionSurface, startT, outResult); updateCaptionInsets(params, wct, outResult, taskBounds); updateTaskSurface(params, startT, finishT, outResult); + Trace.endSection(); + + outResult.mRootView.setPadding(0, params.mCaptionTopPadding, 0, 0); + updateViewHierarchy(params, outResult, startT); + Trace.endSection(); // WindowDecoration#relayout } private void inflateIfNeeded(RelayoutParams params, WindowContainerTransaction wct, @@ -277,15 +305,44 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mDecorWindowContext = mContext.createConfigurationContext(mWindowDecorConfig); mDecorWindowContext.setTheme(mContext.getThemeResId()); if (params.mLayoutResId != 0) { - outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext) - .inflate(params.mLayoutResId, null); + outResult.mRootView = inflateLayout(mDecorWindowContext, params.mLayoutResId); } } if (outResult.mRootView == null) { - outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext) - .inflate(params.mLayoutResId, null); + outResult.mRootView = inflateLayout(mDecorWindowContext, params.mLayoutResId); + } + } + + @VisibleForTesting + T inflateLayout(Context context, int layoutResId) { + return (T) LayoutInflater.from(context).inflate(layoutResId, null); + } + + private void updateViewHierarchy(@NonNull RelayoutParams params, + @NonNull RelayoutResult<T> outResult, @NonNull SurfaceControl.Transaction startT) { + Trace.beginSection("WindowDecoration#updateViewHierarchy"); + final WindowManager.LayoutParams lp = + new WindowManager.LayoutParams( + outResult.mCaptionWidth, + outResult.mCaptionHeight, + TYPE_APPLICATION, + FLAG_NOT_FOCUSABLE | FLAG_SPLIT_TOUCH, + PixelFormat.TRANSPARENT); + lp.setTitle("Caption of Task=" + mTaskInfo.taskId); + lp.setTrustedOverlay(); + lp.inputFeatures = params.mInputFeatures; + if (params.mAsyncViewHost) { + if (params.mApplyStartTransactionOnDraw) { + throw new IllegalArgumentException( + "We cannot both sync viewhost ondraw and delay viewhost creation."); + } + mDecorViewHost.updateViewAsync(outResult.mRootView, lp, mTaskInfo.getConfiguration()); + } else { + mDecorViewHost.updateView(outResult.mRootView, lp, mTaskInfo.getConfiguration(), + params.mApplyStartTransactionOnDraw ? startT : null); } + Trace.endSection(); } private void updateDecorationContainerSurface( @@ -308,23 +365,14 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .show(mDecorationContainerSurface); } - private void updateCaptionContainerSurface( + private void updateCaptionContainerSurface(@NonNull SurfaceControl captionSurface, SurfaceControl.Transaction startT, RelayoutResult<T> outResult) { - if (mCaptionContainerSurface == null) { - final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); - mCaptionContainerSurface = builder - .setName("Caption container of Task=" + mTaskInfo.taskId) - .setContainerLayer() - .setParent(mDecorationContainerSurface) - .setCallsite("WindowDecoration.updateCaptionContainerSurface") - .build(); - } - - startT.setWindowCrop(mCaptionContainerSurface, outResult.mCaptionWidth, + startT.reparent(captionSurface, mDecorationContainerSurface) + .setWindowCrop(captionSurface, outResult.mCaptionWidth, outResult.mCaptionHeight) - .setPosition(mCaptionContainerSurface, outResult.mCaptionX, 0 /* y */) - .setLayer(mCaptionContainerSurface, CAPTION_LAYER_Z_ORDER) - .show(mCaptionContainerSurface); + .setPosition(captionSurface, outResult.mCaptionX, 0 /* y */) + .setLayer(captionSurface, CAPTION_LAYER_Z_ORDER) + .show(captionSurface); } private void updateCaptionInsets(RelayoutParams params, WindowContainerTransaction wct, @@ -373,7 +421,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } final WindowDecorationInsets newInsets = new WindowDecorationInsets( - mTaskInfo.token, mOwner, captionInsetsRect, boundingRects); + mTaskInfo.token, mOwner, captionInsetsRect, boundingRects, + params.mInsetSourceFlags); if (!newInsets.equals(mWindowDecorationInsets)) { // Add or update this caption as an insets source. mWindowDecorationInsets = newInsets; @@ -417,59 +466,6 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } } - /** - * Updates a {@link SurfaceControlViewHost} to connect the window decoration surfaces with our - * View hierarchy. - * - * @param params parameters to use from the last relayout - * @param onDrawTransaction a transaction to apply in sync with #onDraw - * @param outResult results to use from the last relayout - * - */ - protected void updateViewHost(RelayoutParams params, - SurfaceControl.Transaction onDrawTransaction, RelayoutResult<T> outResult) { - Trace.beginSection("CaptionViewHostLayout"); - if (mCaptionWindowManager == null) { - // Put caption under a container surface because ViewRootImpl sets the destination frame - // of windowless window layers and BLASTBufferQueue#update() doesn't support offset. - mCaptionWindowManager = new WindowlessWindowManager( - mTaskInfo.getConfiguration(), mCaptionContainerSurface, - null /* hostInputToken */); - } - mCaptionWindowManager.setConfiguration(mTaskInfo.getConfiguration()); - final WindowManager.LayoutParams lp = - new WindowManager.LayoutParams(outResult.mCaptionWidth, outResult.mCaptionHeight, - TYPE_APPLICATION, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); - lp.setTitle("Caption of Task=" + mTaskInfo.taskId); - lp.setTrustedOverlay(); - lp.inputFeatures = params.mInputFeatures; - if (mViewHost == null) { - Trace.beginSection("CaptionViewHostLayout-new"); - mViewHost = mSurfaceControlViewHostFactory.create(mDecorWindowContext, mDisplay, - mCaptionWindowManager); - if (params.mApplyStartTransactionOnDraw) { - if (onDrawTransaction == null) { - throw new IllegalArgumentException("Trying to sync a null Transaction"); - } - mViewHost.getRootSurfaceControl().applyTransactionOnDraw(onDrawTransaction); - } - mViewHost.setView(outResult.mRootView, lp); - Trace.endSection(); - } else { - Trace.beginSection("CaptionViewHostLayout-relayout"); - if (params.mApplyStartTransactionOnDraw) { - if (onDrawTransaction == null) { - throw new IllegalArgumentException("Trying to sync a null Transaction"); - } - mViewHost.getRootSurfaceControl().applyTransactionOnDraw(onDrawTransaction); - } - mViewHost.relayout(lp); - Trace.endSection(); - } - Trace.endSection(); // CaptionViewHostLayout - } - private Rect calculateBoundingRect(@NonNull OccludingCaptionElement element, int elementWidthPx, @NonNull Rect captionRect) { switch (element.mAlignment) { @@ -484,28 +480,46 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> throw new IllegalArgumentException("Unexpected alignment " + element.mAlignment); } - /** - * Checks if task has entered/exited immersive mode and requires a change in caption visibility. - */ - private void updateCaptionVisibility(View rootView, int displayId) { - final InsetsState insetsState = mDisplayController.getInsetsState(displayId); - for (int i = 0; i < insetsState.sourceSize(); i++) { - final InsetsSource source = insetsState.sourceAt(i); - if (source.getType() != statusBars()) { - continue; - } + void onKeyguardStateChanged(boolean visible, boolean occluded) { + final boolean prevVisAndOccluded = mIsKeyguardVisibleAndOccluded; + mIsKeyguardVisibleAndOccluded = visible && occluded; + final boolean changed = prevVisAndOccluded != mIsKeyguardVisibleAndOccluded; + if (changed) { + relayout(mTaskInfo); + } + } - mIsCaptionVisible = source.isVisible(); - setCaptionVisibility(rootView, mIsCaptionVisible); + void onInsetsStateChanged(@NonNull InsetsState insetsState) { + final boolean prevStatusBarVisibility = mIsStatusBarVisible; + mIsStatusBarVisible = InsetsStateKt.isVisible(insetsState, statusBars()); + final boolean changed = prevStatusBarVisibility != mIsStatusBarVisible; - return; + if (changed) { + relayout(mTaskInfo); } } + /** + * Checks if task has entered/exited immersive mode and requires a change in caption visibility. + */ + private void updateCaptionVisibility(View rootView) { + // Caption should always be visible in freeform mode. When not in freeform, align with the + // status bar except when showing over keyguard (where it should not shown). + // TODO(b/356405803): Investigate how it's possible for the status bar visibility to be + // false while a freeform window is open if the status bar is always forcibly-shown. It + // may be that the InsetsState (from which |mIsStatusBarVisible| is set) still contains + // an invisible insets source in immersive cases even if the status bar is shown? + mIsCaptionVisible = mTaskInfo.isFreeform() + || (mIsStatusBarVisible && !mIsKeyguardVisibleAndOccluded); + setCaptionVisibility(rootView, mIsCaptionVisible); + } + void setTaskDragResizer(TaskDragResizer taskDragResizer) { mTaskDragResizer = taskDragResizer; } + // TODO(b/346441962): Move these three methods closer to implementing or View-level classes to + // keep implementation details more encapsulated. private void setCaptionVisibility(View rootView, boolean visible) { if (rootView == null) { return; @@ -539,18 +553,11 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } void releaseViews(WindowContainerTransaction wct) { - if (mViewHost != null) { - mViewHost.release(); - mViewHost = null; - } - - mCaptionWindowManager = null; - final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); boolean released = false; - if (mCaptionContainerSurface != null) { - t.remove(mCaptionContainerSurface); - mCaptionContainerSurface = null; + if (mDecorViewHost != null) { + mWindowDecorViewHostSupplier.release(mDecorViewHost, t); + mDecorViewHost = null; released = true; } @@ -606,7 +613,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> * Create a window associated with this WindowDecoration. * Note that subclass must dispose of this when the task is hidden/closed. * - * @param layoutId layout to make the window from + * @param v View to attach to the window * @param t the transaction to apply * @param xPos x position of new window * @param yPos y position of new window @@ -614,9 +621,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> * @param height height of new window * @return the {@link AdditionalViewHostViewContainer} that was added. */ - AdditionalViewHostViewContainer addWindow(int layoutId, String namePrefix, - SurfaceControl.Transaction t, SurfaceSyncGroup ssg, int xPos, int yPos, - int width, int height) { + AdditionalViewHostViewContainer addWindow(@NonNull View v, @NonNull String namePrefix, + @NonNull SurfaceControl.Transaction t, @NonNull SurfaceSyncGroup ssg, + int xPos, int yPos, int width, int height) { final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); SurfaceControl windowSurfaceControl = builder .setName(namePrefix + " of Task=" + mTaskInfo.taskId) @@ -624,14 +631,15 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .setParent(mDecorationContainerSurface) .setCallsite("WindowDecoration.addWindow") .build(); - View v = LayoutInflater.from(mDecorWindowContext).inflate(layoutId, null); - t.setPosition(windowSurfaceControl, xPos, yPos) .setWindowCrop(windowSurfaceControl, width, height) .show(windowSurfaceControl); final WindowManager.LayoutParams lp = - new WindowManager.LayoutParams(width, height, TYPE_APPLICATION, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + new WindowManager.LayoutParams( + width, + height, + TYPE_APPLICATION, + FLAG_NOT_FOCUSABLE | FLAG_WATCH_OUTSIDE_TOUCH | FLAG_SPLIT_TOUCH, PixelFormat.TRANSPARENT); lp.setTitle("Additional window of Task=" + mTaskInfo.taskId); lp.setTrustedOverlay(); @@ -645,6 +653,25 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } /** + * Create a window associated with this WindowDecoration. + * Note that subclass must dispose of this when the task is hidden/closed. + * + * @param layoutId layout to make the window from + * @param t the transaction to apply + * @param xPos x position of new window + * @param yPos y position of new window + * @param width width of new window + * @param height height of new window + * @return the {@link AdditionalViewHostViewContainer} that was added. + */ + AdditionalViewHostViewContainer addWindow(int layoutId, String namePrefix, + SurfaceControl.Transaction t, SurfaceSyncGroup ssg, int xPos, int yPos, + int width, int height) { + final View v = LayoutInflater.from(mDecorWindowContext).inflate(layoutId, null); + return addWindow(v, namePrefix, t, ssg, xPos, yPos, width, height); + } + + /** * Adds caption inset source to a WCT */ public void addCaptionInset(WindowContainerTransaction wct) { @@ -656,7 +683,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> final int captionHeight = loadDimensionPixelSize(mContext.getResources(), captionHeightId); final Rect captionInsets = new Rect(0, 0, 0, captionHeight); final WindowDecorationInsets newInsets = new WindowDecorationInsets(mTaskInfo.token, - mOwner, captionInsets, null /* boundingRets */); + mOwner, captionInsets, null /* boundingRets */, 0 /* flags */); if (!newInsets.equals(mWindowDecorationInsets)) { mWindowDecorationInsets = newInsets; mWindowDecorationInsets.addOrUpdate(wct); @@ -670,14 +697,18 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> int mCaptionWidthId; final List<OccludingCaptionElement> mOccludingCaptionElements = new ArrayList<>(); int mInputFeatures; + @InsetsSource.Flags int mInsetSourceFlags; int mShadowRadiusId; int mCornerRadius; + int mCaptionTopPadding; + Configuration mWindowDecorConfig; boolean mApplyStartTransactionOnDraw; boolean mSetTaskPositionAndCrop; + boolean mAsyncViewHost; void reset() { mLayoutResId = Resources.ID_NULL; @@ -685,12 +716,16 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mCaptionWidthId = Resources.ID_NULL; mOccludingCaptionElements.clear(); mInputFeatures = 0; + mInsetSourceFlags = 0; mShadowRadiusId = Resources.ID_NULL; mCornerRadius = 0; + mCaptionTopPadding = 0; + mApplyStartTransactionOnDraw = false; mSetTaskPositionAndCrop = false; + mAsyncViewHost = false; mWindowDecorConfig = null; } @@ -749,19 +784,22 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> private final Binder mOwner; private final Rect mFrame; private final Rect[] mBoundingRects; + private final @InsetsSource.Flags int mFlags; private WindowDecorationInsets(WindowContainerToken token, Binder owner, Rect frame, - Rect[] boundingRects) { + Rect[] boundingRects, @InsetsSource.Flags int flags) { mToken = token; mOwner = owner; mFrame = frame; mBoundingRects = boundingRects; + mFlags = flags; } void addOrUpdate(WindowContainerTransaction wct) { - wct.addInsetsSource(mToken, mOwner, INDEX, captionBar(), mFrame, mBoundingRects); + wct.addInsetsSource(mToken, mOwner, INDEX, captionBar(), mFrame, mBoundingRects, + mFlags); wct.addInsetsSource(mToken, mOwner, INDEX, mandatorySystemGestures(), mFrame, - mBoundingRects); + mBoundingRects, 0 /* flags */); } void remove(WindowContainerTransaction wct) { @@ -775,12 +813,13 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> if (!(o instanceof WindowDecoration.WindowDecorationInsets that)) return false; return Objects.equals(mToken, that.mToken) && Objects.equals(mOwner, that.mOwner) && Objects.equals(mFrame, that.mFrame) - && Objects.deepEquals(mBoundingRects, that.mBoundingRects); + && Objects.deepEquals(mBoundingRects, that.mBoundingRects) + && mFlags == that.mFlags; } @Override public int hashCode() { - return Objects.hash(mToken, mOwner, mFrame, Arrays.hashCode(mBoundingRects)); + return Objects.hash(mToken, mOwner, mFrame, Arrays.hashCode(mBoundingRects), mFlags); } } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowManagerWrapper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowManagerWrapper.kt new file mode 100644 index 000000000000..5c2ff1b1647c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowManagerWrapper.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 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.windowdecor + +import android.view.View +import android.view.WindowManager + +/** + * A wrapper for [WindowManager] to make view manipulation operations related to window + * decors more testable. + */ +class WindowManagerWrapper ( + private val windowManager: WindowManager +){ + + fun addView(v: View, lp: WindowManager.LayoutParams) { + windowManager.addView(v, lp) + } + + fun removeViewImmediate(v: View) { + windowManager.removeViewImmediate(v) + } + + fun updateViewLayout(v: View, lp: WindowManager.LayoutParams) { + windowManager.updateViewLayout(v, lp) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt index 6c2c8fd46bc9..226b0fb2e1a1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt @@ -16,44 +16,88 @@ package com.android.wm.shell.windowdecor.additionalviewcontainer +import android.annotation.LayoutRes import android.content.Context import android.graphics.PixelFormat +import android.view.Gravity import android.view.LayoutInflater import android.view.SurfaceControl import android.view.View import android.view.WindowManager +import com.android.wm.shell.windowdecor.WindowManagerWrapper /** * An [AdditionalViewContainer] that uses the system [WindowManager] instance. Intended * for view containers that should be above the status bar layer. */ class AdditionalSystemViewContainer( - private val context: Context, - layoutId: Int, + private val windowManagerWrapper: WindowManagerWrapper, taskId: Int, x: Int, y: Int, width: Int, - height: Int -) : AdditionalViewContainer() { + height: Int, + flags: Int, override val view: View +) : AdditionalViewContainer() { + val lp: WindowManager.LayoutParams = WindowManager.LayoutParams( + width, height, x, y, + WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL, + flags, + PixelFormat.TRANSPARENT + ).apply { + title = "Additional view container of Task=$taskId" + gravity = Gravity.LEFT or Gravity.TOP + setTrustedOverlay() + } + + constructor( + context: Context, + windowManagerWrapper: WindowManagerWrapper, + taskId: Int, + x: Int, + y: Int, + width: Int, + height: Int, + flags: Int, + @LayoutRes layoutId: Int + ) : this( + windowManagerWrapper = windowManagerWrapper, + taskId = taskId, + x = x, + y = y, + width = width, + height = height, + flags = flags, + view = LayoutInflater.from(context).inflate(layoutId, null /* parent */) + ) + + constructor( + context: Context, + windowManagerWrapper: WindowManagerWrapper, + taskId: Int, + x: Int, + y: Int, + width: Int, + height: Int, + flags: Int + ) : this( + windowManagerWrapper = windowManagerWrapper, + taskId = taskId, + x = x, + y = y, + width = width, + height = height, + flags = flags, + view = View(context) + ) init { - view = LayoutInflater.from(context).inflate(layoutId, null) - val lp = WindowManager.LayoutParams( - width, height, x, y, - WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - PixelFormat.TRANSPARENT - ) - lp.title = "Additional view container of Task=$taskId" - lp.setTrustedOverlay() - val wm: WindowManager? = context.getSystemService(WindowManager::class.java) - wm?.addView(view, lp) + windowManagerWrapper.addView(view, lp) } override fun releaseView() { - context.getSystemService(WindowManager::class.java)?.removeViewImmediate(view) + windowManagerWrapper.removeViewImmediate(view) } override fun setPosition(t: SurfaceControl.Transaction, x: Float, y: Float) { @@ -61,6 +105,6 @@ class AdditionalSystemViewContainer( this.x = x.toInt() this.y = y.toInt() } - view.layoutParams = lp + windowManagerWrapper.updateViewLayout(view, lp) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/InsetsState.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/InsetsState.kt new file mode 100644 index 000000000000..be01a20f9307 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/InsetsState.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 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.windowdecor.extension + +import android.view.InsetsState +import android.view.WindowInsets + +/** + * Whether the source of the given [type] is visible or false if there is no source of that type. + */ +fun InsetsState.isVisible(@WindowInsets.Type.InsetsType type: Int): Boolean { + for (i in 0 until sourceSize()) { + val source = sourceAt(i) + if (source.type != type) { + continue + } + return source.isVisible + } + return false +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt index 7ade9876d28a..6f8e00143848 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt @@ -18,6 +18,8 @@ package com.android.wm.shell.windowdecor.extension import android.app.TaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.app.WindowConfiguration.WINDOWING_MODE_PINNED import android.view.WindowInsetsController.APPEARANCE_LIGHT_CAPTION_BARS import android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND @@ -33,5 +35,14 @@ val TaskInfo.isLightCaptionBarAppearance: Boolean return (appearance and APPEARANCE_LIGHT_CAPTION_BARS) != 0 } +/** Whether the task is in fullscreen windowing mode. */ val TaskInfo.isFullscreen: Boolean get() = windowingMode == WINDOWING_MODE_FULLSCREEN + +/** Whether the task is in pinned windowing mode. */ +val TaskInfo.isPinned: Boolean + get() = windowingMode == WINDOWING_MODE_PINNED + +/** Whether the task is in multi-window windowing mode. */ +val TaskInfo.isMultiWindow: Boolean + get() = windowingMode == WINDOWING_MODE_MULTI_WINDOW diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt index 8d822c252288..9c7d644afb7e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt @@ -20,28 +20,48 @@ import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.res.ColorStateList import android.graphics.Color +import android.graphics.Point +import android.hardware.input.InputManager +import android.os.Handler +import android.view.MotionEvent.ACTION_DOWN +import android.view.SurfaceControl import android.view.View +import android.view.View.OnClickListener import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS +import android.view.WindowManager import android.widget.ImageButton +import com.android.internal.policy.SystemBarUtils +import com.android.window.flags.Flags import com.android.wm.shell.R -import com.android.wm.shell.animation.Interpolators +import com.android.wm.shell.shared.animation.Interpolators +import com.android.wm.shell.windowdecor.WindowManagerWrapper +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer /** * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen/split). * It hosts a simple handle bar from which to initiate a drag motion to enter desktop mode. */ internal class AppHandleViewHolder( - rootView: View, - onCaptionTouchListener: View.OnTouchListener, - onCaptionButtonClickListener: View.OnClickListener + rootView: View, + onCaptionTouchListener: View.OnTouchListener, + onCaptionButtonClickListener: OnClickListener, + private val windowManagerWrapper: WindowManagerWrapper, + private val handler: Handler ) : WindowDecorationViewHolder(rootView) { companion object { private const val CAPTION_HANDLE_ANIMATION_DURATION: Long = 100 } - + private lateinit var taskInfo: RunningTaskInfo private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption) private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle) + private val inputManager = context.getSystemService(InputManager::class.java) + private var statusBarInputLayerExists = false + + // An invisible View that takes up the same coordinates as captionHandle but is layered + // above the status bar. The purpose of this View is to receive input intended for + // captionHandle. + private var statusBarInputLayer: AdditionalSystemViewContainer? = null init { captionView.setOnTouchListener(onCaptionTouchListener) @@ -49,8 +69,31 @@ internal class AppHandleViewHolder( captionHandle.setOnClickListener(onCaptionButtonClickListener) } - override fun bindData(taskInfo: RunningTaskInfo) { + override fun bindData( + taskInfo: RunningTaskInfo, + position: Point, + width: Int, + height: Int, + isCaptionVisible: Boolean + ) { captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo)) + this.taskInfo = taskInfo + // If handle is not in status bar region(i.e., bottom stage in vertical split), + // do not create an input layer + if (position.y >= SystemBarUtils.getStatusBarHeight(context)) return + if (!isCaptionVisible && statusBarInputLayerExists) { + disposeStatusBarInputLayer() + return + } + // Input layer view creation / modification takes a significant amount of time; + // post them so we don't hold up DesktopModeWindowDecoration#relayout. + if (statusBarInputLayerExists) { + handler.post { updateStatusBarInputLayer(position) } + } else { + // Input layer is created on a delay; prevent multiple from being created. + statusBarInputLayerExists = true + handler.post { createStatusBarInputLayer(position, width, height) } + } } override fun onHandleMenuOpened() { @@ -61,6 +104,59 @@ internal class AppHandleViewHolder( animateCaptionHandleAlpha(startValue = 0f, endValue = 1f) } + private fun createStatusBarInputLayer(handlePosition: Point, + handleWidth: Int, + handleHeight: Int) { + if (!Flags.enableHandleInputFix()) return + statusBarInputLayer = AdditionalSystemViewContainer(context, windowManagerWrapper, + taskInfo.taskId, handlePosition.x, handlePosition.y, handleWidth, handleHeight, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + ) + val view = statusBarInputLayer?.view ?: error("Unable to find statusBarInputLayer View") + val lp = statusBarInputLayer?.lp ?: error("Unable to find statusBarInputLayer " + + "LayoutParams") + lp.title = "Handle Input Layer of task " + taskInfo.taskId + lp.setTrustedOverlay() + // Make this window a spy window to enable it to pilfer pointers from the system-wide + // gesture listener that receives events before window. This is to prevent notification + // shade gesture when we swipe down to enter desktop. + lp.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY + view.setOnHoverListener { _, event -> + captionHandle.onHoverEvent(event) + } + // Caption handle is located within the status bar region, meaning the + // DisplayPolicy will attempt to transfer this input to status bar if it's + // a swipe down. Pilfer here to keep the gesture in handle alone. + view.setOnTouchListener { v, event -> + if (event.actionMasked == ACTION_DOWN) { + inputManager.pilferPointers(v.viewRootImpl.inputToken) + } + captionHandle.dispatchTouchEvent(event) + return@setOnTouchListener true + } + windowManagerWrapper.updateViewLayout(view, lp) + } + + private fun updateStatusBarInputLayer(globalPosition: Point) { + statusBarInputLayer?.setPosition( + SurfaceControl.Transaction(), + globalPosition.x.toFloat(), + globalPosition.y.toFloat() + ) ?: return + } + + /** + * Remove the input layer from [WindowManager]. Should be used when caption handle + * is not visible. + */ + fun disposeStatusBarInputLayer() { + statusBarInputLayerExists = false + handler.post { + statusBarInputLayer?.releaseView() + statusBarInputLayer = null + } + } + private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int { return if (shouldUseLightCaptionColors(taskInfo)) { context.getColor(R.color.desktop_mode_caption_handle_bar_light) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt index 46127b177bc3..e9961655d979 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt @@ -21,12 +21,16 @@ import android.content.res.ColorStateList import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Color +import android.graphics.Point +import android.graphics.Rect import android.graphics.drawable.LayerDrawable import android.graphics.drawable.RippleDrawable import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.shapes.RoundRectShape import android.view.View import android.view.View.OnLongClickListener +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import android.view.accessibility.AccessibilityEvent import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView @@ -34,6 +38,7 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.ui.graphics.toArgb import androidx.core.content.withStyledAttributes +import androidx.core.view.isGone import androidx.core.view.isVisible import com.android.internal.R.attr.materialColorOnSecondaryContainer import com.android.internal.R.attr.materialColorOnSurface @@ -41,8 +46,9 @@ import com.android.internal.R.attr.materialColorSecondaryContainer import com.android.internal.R.attr.materialColorSurfaceContainerHigh import com.android.internal.R.attr.materialColorSurfaceContainerLow import com.android.internal.R.attr.materialColorSurfaceDim -import com.android.window.flags.Flags +import com.android.window.flags.Flags.enableMinimizeButton import com.android.wm.shell.R +import android.window.flags.DesktopModeFlags import com.android.wm.shell.windowdecor.MaximizeButtonView import com.android.wm.shell.windowdecor.common.DecorThemeUtil import com.android.wm.shell.windowdecor.common.OPACITY_100 @@ -59,7 +65,7 @@ import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppeara * finer controls such as a close window button and an "app info" section to pull up additional * controls. */ -internal class AppHeaderViewHolder( +class AppHeaderViewHolder( rootView: View, onCaptionTouchListener: View.OnTouchListener, onCaptionButtonClickListener: View.OnClickListener, @@ -81,9 +87,9 @@ internal class AppHeaderViewHolder( .getDimensionPixelSize(R.dimen.desktop_mode_header_buttons_ripple_radius) /** - * The app chip, maximize and close button's height extends to the top & bottom edges of the - * header, and their width may be larger than their height. This is by design to increase the - * clickable and hover-able bounds of the view as much as possible. However, to prevent the + * The app chip, minimize, maximize and close button's height extends to the top & bottom edges + * of the header, and their width may be larger than their height. This is by design to increase + * the clickable and hover-able bounds of the view as much as possible. However, to prevent the * ripple drawable from being as large as the views (and asymmetrical), insets are applied to * the background ripple drawable itself to give the appearance of a smaller button * (with padding between itself and the header edges / sibling buttons) but without affecting @@ -93,6 +99,12 @@ internal class AppHeaderViewHolder( vertical = context.resources .getDimensionPixelSize(R.dimen.desktop_mode_header_app_chip_ripple_inset_vertical) ) + private val minimizeDrawableInsets = DrawableInsets( + vertical = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_header_minimize_ripple_inset_vertical), + horizontal = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_header_minimize_ripple_inset_horizontal) + ) private val maximizeDrawableInsets = DrawableInsets( vertical = context.resources .getDimensionPixelSize(R.dimen.desktop_mode_header_maximize_ripple_inset_vertical), @@ -114,6 +126,7 @@ internal class AppHeaderViewHolder( private val maximizeButtonView: MaximizeButtonView = rootView.requireViewById(R.id.maximize_button_view) private val maximizeWindowButton: ImageButton = rootView.requireViewById(R.id.maximize_window) + private val minimizeWindowButton: ImageButton = rootView.requireViewById(R.id.minimize_window) private val appNameTextView: TextView = rootView.requireViewById(R.id.application_name) private val appIconImageView: ImageView = rootView.requireViewById(R.id.application_icon) val appNameTextWidth: Int @@ -130,14 +143,22 @@ internal class AppHeaderViewHolder( maximizeWindowButton.setOnGenericMotionListener(onCaptionGenericMotionListener) maximizeWindowButton.onLongClickListener = onLongClickListener closeWindowButton.setOnTouchListener(onCaptionTouchListener) + minimizeWindowButton.setOnClickListener(onCaptionButtonClickListener) + minimizeWindowButton.setOnTouchListener(onCaptionTouchListener) appNameTextView.text = appName appIconImageView.setImageBitmap(appIconBitmap) maximizeButtonView.onHoverAnimationFinishedListener = onMaximizeHoverAnimationFinishedListener } - override fun bindData(taskInfo: RunningTaskInfo) { - if (Flags.enableThemedAppHeaders()) { + override fun bindData( + taskInfo: RunningTaskInfo, + position: Point, + width: Int, + height: Int, + isCaptionVisible: Boolean + ) { + if (DesktopModeFlags.ENABLE_THEMED_APP_HEADERS.isTrue()) { bindDataWithThemedHeaders(taskInfo) } else { bindDataLegacy(taskInfo) @@ -150,11 +171,13 @@ internal class AppHeaderViewHolder( val alpha = Color.alpha(color) closeWindowButton.imageTintList = ColorStateList.valueOf(color) maximizeWindowButton.imageTintList = ColorStateList.valueOf(color) + minimizeWindowButton.imageTintList = ColorStateList.valueOf(color) expandMenuButton.imageTintList = ColorStateList.valueOf(color) appNameTextView.isVisible = !taskInfo.isTransparentCaptionBarAppearance appNameTextView.setTextColor(color) appIconImageView.imageAlpha = alpha maximizeWindowButton.imageAlpha = alpha + minimizeWindowButton.imageAlpha = alpha closeWindowButton.imageAlpha = alpha expandMenuButton.imageAlpha = alpha context.withStyledAttributes( @@ -169,8 +192,10 @@ internal class AppHeaderViewHolder( openMenuButton.background = getDrawable(0) maximizeWindowButton.background = getDrawable(1) closeWindowButton.background = getDrawable(1) + minimizeWindowButton.background = getDrawable(1) } maximizeButtonView.setAnimationTints(isDarkMode()) + minimizeWindowButton.isGone = !enableMinimizeButton() } private fun bindDataWithThemedHeaders(taskInfo: RunningTaskInfo) { @@ -205,6 +230,16 @@ internal class AppHeaderViewHolder( } appIconImageView.imageAlpha = foregroundAlpha } + // Minimize button. + minimizeWindowButton.apply { + imageTintList = colorStateList + background = createRippleDrawable( + color = foregroundColor, + cornerRadius = headerButtonsRippleRadius, + drawableInsets = minimizeDrawableInsets + ) + } + minimizeWindowButton.isGone = !enableMinimizeButton() // Maximize button. maximizeButtonView.setAnimationTints( darkMode = header.appTheme == Theme.DARK, @@ -229,14 +264,18 @@ internal class AppHeaderViewHolder( override fun onHandleMenuOpened() {} - override fun onHandleMenuClosed() {} + override fun onHandleMenuClosed() { + openMenuButton.post { + openMenuButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) + } + } - fun setAnimatingTaskResize(animatingTaskResize: Boolean) { - // If animating a task resize, cancel any running hover animations - if (animatingTaskResize) { + fun setAnimatingTaskResizeOrReposition(animatingTaskResizeOrReposition: Boolean) { + // If animating a task resize or reposition, cancel any running hover animations + if (animatingTaskResizeOrReposition) { maximizeButtonView.cancelHoverAnimation() } - maximizeButtonView.hoverDisabled = animatingTaskResize + maximizeButtonView.hoverDisabled = animatingTaskResizeOrReposition } fun onMaximizeWindowHoverExit() { @@ -247,6 +286,40 @@ internal class AppHeaderViewHolder( maximizeButtonView.startHoverAnimation() } + fun runOnAppChipGlobalLayout(runnable: () -> Unit) { + if (openMenuButton.isAttachedToWindow) { + // App chip is already inflated. + runnable() + return + } + // Wait for app chip to be inflated before notifying repository. + openMenuButton.viewTreeObserver.addOnGlobalLayoutListener(object : + OnGlobalLayoutListener { + override fun onGlobalLayout() { + runnable() + openMenuButton.viewTreeObserver.removeOnGlobalLayoutListener(this) + } + }) + } + + fun getAppChipLocationInWindow(): Rect { + val appChipBoundsInWindow = IntArray(2) + openMenuButton.getLocationInWindow(appChipBoundsInWindow) + + return Rect( + /* left = */ appChipBoundsInWindow[0], + /* top = */ appChipBoundsInWindow[1], + /* right = */ appChipBoundsInWindow[0] + openMenuButton.width, + /* bottom = */ appChipBoundsInWindow[1] + openMenuButton.height + ) + } + + fun requestAccessibilityFocus() { + maximizeWindowButton.post { + maximizeWindowButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) + } + } + private fun getHeaderStyle(header: Header): HeaderStyle { return HeaderStyle( background = getHeaderBackground(header), @@ -497,4 +570,26 @@ internal class AppHeaderViewHolder( private const val LIGHT_THEME_UNFOCUSED_OPACITY = 166 // 65% private const val FOCUSED_OPACITY = 255 } + + class Factory { + fun create( + rootView: View, + onCaptionTouchListener: View.OnTouchListener, + onCaptionButtonClickListener: View.OnClickListener, + onLongClickListener: OnLongClickListener, + onCaptionGenericMotionListener: View.OnGenericMotionListener, + appName: CharSequence, + appIconBitmap: Bitmap, + onMaximizeHoverAnimationFinishedListener: () -> Unit, + ): AppHeaderViewHolder = AppHeaderViewHolder( + rootView, + onCaptionTouchListener, + onCaptionButtonClickListener, + onLongClickListener, + onCaptionGenericMotionListener, + appName, + appIconBitmap, + onMaximizeHoverAnimationFinishedListener, + ) + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt index 5ae8d252a908..5ea55b367703 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt @@ -17,20 +17,27 @@ package com.android.wm.shell.windowdecor.viewholder import android.app.ActivityManager.RunningTaskInfo import android.content.Context +import android.graphics.Point import android.view.View /** * Encapsulates the root [View] of a window decoration and its children to facilitate looking up * children (via findViewById) and updating to the latest data from [RunningTaskInfo]. */ -internal abstract class WindowDecorationViewHolder(rootView: View) { +abstract class WindowDecorationViewHolder(rootView: View) { val context: Context = rootView.context /** * A signal to the view holder that new data is available and that the views should be updated to * reflect it. */ - abstract fun bindData(taskInfo: RunningTaskInfo) + abstract fun bindData( + taskInfo: RunningTaskInfo, + position: Point, + width: Int, + height: Int, + isCaptionVisible: Boolean + ) /** Callback when the handle menu is opened. */ abstract fun onHandleMenuOpened() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt new file mode 100644 index 000000000000..139e6790b744 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2024 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.windowdecor.viewhost + +import android.content.Context +import android.content.res.Configuration +import android.view.Display +import android.view.SurfaceControl +import android.view.SurfaceControlViewHost +import android.view.View +import android.view.WindowManager +import android.view.WindowlessWindowManager +import androidx.tracing.Trace +import com.android.internal.annotations.VisibleForTesting +import com.android.wm.shell.shared.annotations.ShellMainThread +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +typealias SurfaceControlViewHostFactory = + (Context, Display, WindowlessWindowManager, String) -> SurfaceControlViewHost + +/** + * A default implementation of [WindowDecorViewHost] backed by a [SurfaceControlViewHost]. + * + * It does not support swapping the root view added to the VRI of the [SurfaceControlViewHost], and + * any attempts to do will throw, which means that once a [View] is added using [updateView] or + * [updateViewAsync], only its properties and binding may be changed, its children views may be + * added, removed or changed and its [WindowManager.LayoutParams] may be changed. + * It also supports asynchronously updating the view hierarchy using [updateViewAsync], in which + * case the update work will be posted on the [ShellMainThread] with no delay. + */ +class DefaultWindowDecorViewHost( + private val context: Context, + @ShellMainThread private val mainScope: CoroutineScope, + private val display: Display, + private val surfaceControlViewHostFactory: SurfaceControlViewHostFactory = { c, d, wwm, s -> + SurfaceControlViewHost(c, d, wwm, s) + } +) : WindowDecorViewHost { + + private val rootSurface: SurfaceControl = SurfaceControl.Builder() + .setName("DefaultWindowDecorViewHost surface") + .setContainerLayer() + .setCallsite("DefaultWindowDecorViewHost#init") + .build() + + private var wwm: WindowlessWindowManager? = null + @VisibleForTesting + var viewHost: SurfaceControlViewHost? = null + private var currentUpdateJob: Job? = null + + override val surfaceControl: SurfaceControl + get() = rootSurface + + override fun updateView( + view: View, + attrs: WindowManager.LayoutParams, + configuration: Configuration, + onDrawTransaction: SurfaceControl.Transaction? + ) { + Trace.beginSection("DefaultWindowDecorViewHost#updateView") + clearCurrentUpdateJob() + updateViewHost(view, attrs, configuration, onDrawTransaction) + Trace.endSection() + } + + override fun updateViewAsync( + view: View, + attrs: WindowManager.LayoutParams, + configuration: Configuration + ) { + Trace.beginSection("DefaultWindowDecorViewHost#updateViewAsync") + clearCurrentUpdateJob() + currentUpdateJob = mainScope.launch { + updateViewHost(view, attrs, configuration, onDrawTransaction = null) + } + Trace.endSection() + } + + override fun release(t: SurfaceControl.Transaction) { + clearCurrentUpdateJob() + viewHost?.release() + t.remove(rootSurface) + } + + private fun updateViewHost( + view: View, + attrs: WindowManager.LayoutParams, + configuration: Configuration, + onDrawTransaction: SurfaceControl.Transaction? + ) { + Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost") + if (wwm == null) { + wwm = WindowlessWindowManager(configuration, rootSurface, null) + } + requireWindowlessWindowManager().setConfiguration(configuration) + if (viewHost == null) { + viewHost = surfaceControlViewHostFactory.invoke( + context, + display, + requireWindowlessWindowManager(), + "DefaultWindowDecorViewHost#updateViewHost" + ) + } + onDrawTransaction?.let { + requireViewHost().rootSurfaceControl.applyTransactionOnDraw(it) + } + if (requireViewHost().view == null) { + Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost-setView") + requireViewHost().setView(view, attrs) + Trace.endSection() + } else { + check(requireViewHost().view == view) { "Changing view is not allowed" } + Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost-relayout") + requireViewHost().relayout(attrs) + Trace.endSection() + } + Trace.endSection() + } + + private fun clearCurrentUpdateJob() { + currentUpdateJob?.cancel() + currentUpdateJob = null + } + + private fun requireWindowlessWindowManager(): WindowlessWindowManager { + return wwm ?: error("Expected non-null windowless window manager") + } + + private fun requireViewHost(): SurfaceControlViewHost { + return viewHost ?: error("Expected non-null view host") + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostSupplier.kt new file mode 100644 index 000000000000..9997e8f564d8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostSupplier.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 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.windowdecor.viewhost + +import android.content.Context +import android.view.Display +import android.view.SurfaceControl +import com.android.wm.shell.shared.annotations.ShellMainThread +import kotlinx.coroutines.CoroutineScope + +/** + * A supplier of [DefaultWindowDecorViewHost]s. It creates a new one every time one is requested. + */ +class DefaultWindowDecorViewHostSupplier( + @ShellMainThread private val mainScope: CoroutineScope, +) : WindowDecorViewHostSupplier<DefaultWindowDecorViewHost> { + + override fun acquire(context: Context, display: Display): DefaultWindowDecorViewHost { + return DefaultWindowDecorViewHost(context, mainScope, display) + } + + override fun release(viewHost: DefaultWindowDecorViewHost, t: SurfaceControl.Transaction) { + viewHost.release(t) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/WindowDecorViewHost.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/WindowDecorViewHost.kt new file mode 100644 index 000000000000..3fbaea8bd1bf --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/WindowDecorViewHost.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 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.windowdecor.viewhost + +import android.content.res.Configuration +import android.view.SurfaceControl +import android.view.View +import android.view.WindowManager +import com.android.wm.shell.windowdecor.WindowDecoration + +/** + * An interface for a utility that hosts a [WindowDecoration]'s [View] hierarchy under a + * [SurfaceControl]. + */ +interface WindowDecorViewHost { + /** The surface where the underlying [View] hierarchy is being rendered. */ + val surfaceControl: SurfaceControl + + /** Synchronously update the view hierarchy of this view host. */ + fun updateView( + view: View, + attrs: WindowManager.LayoutParams, + configuration: Configuration, + onDrawTransaction: SurfaceControl.Transaction? + ) + + /** Asynchronously update the view hierarchy of this view host. */ + fun updateViewAsync( + view: View, + attrs: WindowManager.LayoutParams, + configuration: Configuration + ) + + /** Releases the underlying [View] hierarchy and removes the backing [SurfaceControl]. */ + fun release(t: SurfaceControl.Transaction) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/WindowDecorViewHostSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/WindowDecorViewHostSupplier.kt new file mode 100644 index 000000000000..0e2358446d12 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/WindowDecorViewHostSupplier.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 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.windowdecor.viewhost + +import android.content.Context +import android.view.Display +import android.view.SurfaceControl + +/** + * An interface for a supplier of [WindowDecorViewHost]s. + */ +interface WindowDecorViewHostSupplier<T : WindowDecorViewHost> { + /** Acquire a [WindowDecorViewHost]. */ + fun acquire(context: Context, display: Display): T + + /** + * Release a [WindowDecorViewHost] when it is no longer used. + * + * @param viewHost the [WindowDecorViewHost] to release + * @param t a transaction that may be used to remove any underlying backing [SurfaceControl] + * that are hosting this [WindowDecorViewHost]. The supplier is not expected to apply + * the transaction. It should be applied by the owner of this supplier. + */ + fun release(viewHost: T, t: SurfaceControl.Transaction) +} diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS index 5df525eb7113..65e50f86e8fe 100644 --- a/libs/WindowManager/Shell/tests/OWNERS +++ b/libs/WindowManager/Shell/tests/OWNERS @@ -1,4 +1,4 @@ -# Bug component: 1157642 +# Bug component: 928594 # includes OWNERS from parent directories natanieljr@google.com pablogamito@google.com diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/OWNERS b/libs/WindowManager/Shell/tests/e2e/desktopmode/OWNERS index 73a5a23909c5..73a5a23909c5 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/OWNERS +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/OWNERS diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp new file mode 100644 index 000000000000..50581f7e01f3 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp @@ -0,0 +1,38 @@ +// +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "WMShellFlickerTestsDesktopMode", + defaults: ["WMShellFlickerTestsDefault"], + manifest: "AndroidManifest.xml", + test_config_template: "AndroidTestTemplate.xml", + srcs: ["src/**/*.kt"], + static_libs: [ + "WMShellFlickerTestsBase", + "WMShellScenariosDesktopMode", + "WMShellTestUtils", + ], + data: ["trace_config/*"], +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidManifest.xml b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidManifest.xml new file mode 100644 index 000000000000..1bbbefadaa03 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidManifest.xml @@ -0,0 +1,77 @@ +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.android.wm.shell.flicker"> + + <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/> + <!-- Read and write traces from external storage --> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + <!-- Allow the test to write directly to /sdcard/ --> + <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> + <!-- Write secure settings --> + <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> + <!-- Capture screen contents --> + <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" /> + <!-- Enable / Disable tracing !--> + <uses-permission android:name="android.permission.DUMP" /> + <!-- Run layers trace --> + <uses-permission android:name="android.permission.HARDWARE_TEST"/> + <!-- Capture screen recording --> + <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT"/> + <!-- Workaround grant runtime permission exception from b/152733071 --> + <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/> + <uses-permission android:name="android.permission.READ_LOGS"/> + <!-- Force-stop test apps --> + <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES"/> + <!-- Control test app's media session --> + <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/> + <!-- ATM.removeRootTasksWithActivityTypes() --> + <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" /> + <!-- Enable bubble notification--> + <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" /> + <!-- Allow the test to connect to perfetto trace processor --> + <uses-permission android:name="android.permission.INTERNET"/> + + <!-- Allow the test to write directly to /sdcard/ and connect to trace processor --> + <application android:requestLegacyExternalStorage="true" + android:networkSecurityConfig="@xml/network_security_config" + android:largeHeap="true"> + <uses-library android:name="android.test.runner"/> + + <service android:name=".NotificationListener" + android:exported="true" + android:label="WMShellTestsNotificationListenerService" + android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"> + <intent-filter> + <action android:name="android.service.notification.NotificationListenerService" /> + </intent-filter> + </service> + + <!-- (b/197936012) Remove startup provider due to test timeout issue --> + <provider + android:name="androidx.startup.InitializationProvider" + android:authorities="${applicationId}.androidx-startup" + tools:node="remove" /> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.wm.shell.flicker" + android:label="WindowManager Shell Flicker Tests"> + </instrumentation> +</manifest> diff --git a/libs/WindowManager/Shell/tests/flicker/service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidTestTemplate.xml index a66dfb4566f9..40dbbac32c7f 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidTestTemplate.xml @@ -59,13 +59,6 @@ <option name="test-file-name" value="{MODULE}.apk"/> <option name="test-file-name" value="FlickerTestApp.apk"/> </target_preparer> - <!-- Enable mocking GPS location by the test app --> - <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> - <option name="run-command" - value="appops set com.android.wm.shell.flicker.pip.apps android:mock_location allow"/> - <option name="teardown-command" - value="appops set com.android.wm.shell.flicker.pip.apps android:mock_location deny"/> - </target_preparer> <!-- Needed for pushing the trace config file --> <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> @@ -97,7 +90,7 @@ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> <option name="pull-pattern-keys" value="perfetto_file_path"/> <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.service/files"/> + value="/data/user/0/com.android.wm.shell.flicker/files"/> <option name="collect-on-run-ended-only" value="true"/> <option name="clean-up" value="true"/> </metrics_collector> diff --git a/libs/WindowManager/Shell/tests/flicker/service/res/xml/network_security_config.xml b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/res/xml/network_security_config.xml index 4bd9ca049f55..4bd9ca049f55 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/res/xml/network_security_config.xml +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/res/xml/network_security_config.xml diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/CloseAllAppWithAppHeaderExitLandscape.kt index 5563bb9fa934..b697d80fd500 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/CloseAllAppWithAppHeaderExitLandscape.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.desktopmode.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,9 +23,9 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_APP -import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_LAST_APP -import com.android.wm.shell.flicker.service.desktopmode.scenarios.CloseAllAppsWithAppHeaderExit +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_APP +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_LAST_APP +import com.android.wm.shell.scenarios.CloseAllAppsWithAppHeaderExit import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/CloseAllAppWithAppHeaderExitPortrait.kt index 3d16d2219c78..a11e876c5bce 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/CloseAllAppWithAppHeaderExitPortrait.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.desktopmode.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,9 +23,9 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_APP -import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_LAST_APP -import com.android.wm.shell.flicker.service.desktopmode.scenarios.CloseAllAppsWithAppHeaderExit +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_APP +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_LAST_APP +import com.android.wm.shell.scenarios.CloseAllAppsWithAppHeaderExit import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt new file mode 100644 index 000000000000..7640cb1fb616 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.PlatformConsts.DESKTOP_MODE_MINIMUM_WINDOW_HEIGHT +import android.tools.PlatformConsts.DESKTOP_MODE_MINIMUM_WINDOW_WIDTH +import android.tools.flicker.AssertionInvocationGroup +import android.tools.flicker.assertors.assertions.AppLayerIncreasesInSize +import android.tools.flicker.assertors.assertions.AppLayerIsInvisibleAtEnd +import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways +import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAtStart +import android.tools.flicker.assertors.assertions.AppWindowBecomesVisible +import android.tools.flicker.assertors.assertions.AppWindowAlignsWithOnlyOneDisplayCornerAtEnd +import android.tools.flicker.assertors.assertions.AppWindowCoversLeftHalfScreenAtEnd +import android.tools.flicker.assertors.assertions.AppWindowCoversRightHalfScreenAtEnd +import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd +import android.tools.flicker.assertors.assertions.AppWindowHasMaxBoundsInOnlyOneDimension +import android.tools.flicker.assertors.assertions.AppWindowHasMaxDisplayHeight +import android.tools.flicker.assertors.assertions.AppWindowHasMaxDisplayWidth +import android.tools.flicker.assertors.assertions.AppWindowHasSizeOfAtLeast +import android.tools.flicker.assertors.assertions.AppWindowInsideDisplayBoundsAtEnd +import android.tools.flicker.assertors.assertions.AppWindowIsInvisibleAtEnd +import android.tools.flicker.assertors.assertions.AppWindowIsVisibleAlways +import android.tools.flicker.assertors.assertions.AppWindowMaintainsAspectRatioAlways +import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd +import android.tools.flicker.assertors.assertions.AppWindowOnTopAtStart +import android.tools.flicker.assertors.assertions.AppWindowRemainInsideDisplayBounds +import android.tools.flicker.assertors.assertions.AppWindowReturnsToStartBoundsAndPosition +import android.tools.flicker.assertors.assertions.LauncherWindowReplacesAppAsTopWindow +import android.tools.flicker.config.AssertionTemplates +import android.tools.flicker.config.FlickerConfigEntry +import android.tools.flicker.config.ScenarioId +import android.tools.flicker.config.desktopmode.Components.DESKTOP_MODE_APP +import android.tools.flicker.config.desktopmode.Components.DESKTOP_WALLPAPER +import android.tools.flicker.config.desktopmode.Components.NON_RESIZABLE_APP +import android.tools.flicker.extractors.ITransitionMatcher +import android.tools.flicker.extractors.ShellTransitionScenarioExtractor +import android.tools.flicker.extractors.TaggedCujTransitionMatcher +import android.tools.flicker.extractors.TaggedScenarioExtractorBuilder +import android.tools.traces.events.CujType +import android.tools.traces.wm.Transition +import android.tools.traces.wm.TransitionType + +class DesktopModeFlickerScenarios { + companion object { + val END_DRAG_TO_DESKTOP = + FlickerConfigEntry( + scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return transitions.filter { + // TODO(351168217) Use jank CUJ to extract a longer trace + it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP + } + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppLayerIsVisibleAlways(DESKTOP_MODE_APP), + AppWindowOnTopAtEnd(DESKTOP_MODE_APP), + AppWindowHasDesktopModeInitialBoundsAtTheEnd(DESKTOP_MODE_APP), + AppWindowBecomesVisible(DESKTOP_WALLPAPER) + ) + .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + // Use this scenario for closing an app in desktop windowing, except the last app. For the + // last app use CLOSE_LAST_APP scenario + val CLOSE_APP = + FlickerConfigEntry( + scenarioId = ScenarioId("CLOSE_APP"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + // In case there are multiple windows closing, filter out the + // last window closing. It should use the CLOSE_LAST_APP + // scenario below. + return transitions + .filter { it.type == TransitionType.CLOSE } + .sortedByDescending { it.id } + .drop(1) + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppWindowOnTopAtStart(DESKTOP_MODE_APP), + AppLayerIsVisibleAtStart(DESKTOP_MODE_APP), + AppLayerIsInvisibleAtEnd(DESKTOP_MODE_APP) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val CLOSE_LAST_APP = + FlickerConfigEntry( + scenarioId = ScenarioId("CLOSE_LAST_APP"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + val lastTransition = + transitions + .filter { it.type == TransitionType.CLOSE } + .maxByOrNull { it.id }!! + return listOf(lastTransition) + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppWindowIsInvisibleAtEnd(DESKTOP_MODE_APP), + LauncherWindowReplacesAppAsTopWindow(DESKTOP_MODE_APP), + AppWindowIsInvisibleAtEnd(DESKTOP_WALLPAPER) + ) + .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val CORNER_RESIZE = + FlickerConfigEntry( + scenarioId = ScenarioId("CORNER_RESIZE"), + extractor = + TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DESKTOP_MODE_RESIZE_WINDOW) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = false) + ) + .build(), + assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + ) + + val EDGE_RESIZE = + FlickerConfigEntry( + scenarioId = ScenarioId("EDGE_RESIZE"), + extractor = + TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DESKTOP_MODE_RESIZE_WINDOW) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = false) + ) + .build(), + assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + + listOf( + AppLayerIncreasesInSize(DESKTOP_MODE_APP), + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val CORNER_RESIZE_TO_MINIMUM_SIZE = + FlickerConfigEntry( + scenarioId = ScenarioId("CORNER_RESIZE_TO_MINIMUM_SIZE"), + extractor = + TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DESKTOP_MODE_RESIZE_WINDOW) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = false) + ) + .build(), + assertions = + AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + + listOf( + AppWindowHasSizeOfAtLeast( + DESKTOP_MODE_APP, + DESKTOP_MODE_MINIMUM_WINDOW_WIDTH, + DESKTOP_MODE_MINIMUM_WINDOW_HEIGHT + ) + ) + .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val CORNER_RESIZE_TO_MAXIMUM_SIZE = + FlickerConfigEntry( + scenarioId = ScenarioId("CORNER_RESIZE_TO_MAXIMUM_SIZE"), + extractor = + TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DESKTOP_MODE_RESIZE_WINDOW) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = false) + ) + .build(), + assertions = + AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + + listOf( + AppLayerIncreasesInSize(DESKTOP_MODE_APP), + AppWindowHasMaxDisplayHeight(DESKTOP_MODE_APP), + AppWindowHasMaxDisplayWidth(DESKTOP_MODE_APP) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val SNAP_RESIZE_LEFT_WITH_BUTTON = + FlickerConfigEntry( + scenarioId = ScenarioId("SNAP_RESIZE_LEFT_WITH_BUTTON"), + extractor = + TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DESKTOP_MODE_SNAP_RESIZE) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = false) + ) + .build(), + assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + + listOf(AppWindowCoversLeftHalfScreenAtEnd(DESKTOP_MODE_APP)) + .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val SNAP_RESIZE_RIGHT_WITH_BUTTON = + FlickerConfigEntry( + scenarioId = ScenarioId("SNAP_RESIZE_RIGHT_WITH_BUTTON"), + extractor = + TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DESKTOP_MODE_SNAP_RESIZE) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = false) + ) + .build(), + assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + + listOf(AppWindowCoversRightHalfScreenAtEnd(DESKTOP_MODE_APP)) + .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val SNAP_RESIZE_LEFT_WITH_DRAG = + FlickerConfigEntry( + scenarioId = ScenarioId("SNAP_RESIZE_LEFT_WITH_DRAG"), + extractor = + TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DESKTOP_MODE_SNAP_RESIZE) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = false) + ) + .build(), + assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + + listOf(AppWindowCoversLeftHalfScreenAtEnd(DESKTOP_MODE_APP)) + .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val SNAP_RESIZE_RIGHT_WITH_DRAG = + FlickerConfigEntry( + scenarioId = ScenarioId("SNAP_RESIZE_RIGHT_WITH_DRAG"), + extractor = + TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DESKTOP_MODE_SNAP_RESIZE) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = false) + ) + .build(), + assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + + listOf(AppWindowCoversRightHalfScreenAtEnd(DESKTOP_MODE_APP)) + .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE = + FlickerConfigEntry( + scenarioId = ScenarioId("SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE"), + extractor = + TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DESKTOP_MODE_SNAP_RESIZE) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = false) + ) + .build(), + assertions = listOf( + AppWindowIsVisibleAlways(NON_RESIZABLE_APP), + AppWindowOnTopAtEnd(NON_RESIZABLE_APP), + AppWindowRemainInsideDisplayBounds(NON_RESIZABLE_APP), + AppWindowMaintainsAspectRatioAlways(NON_RESIZABLE_APP), + AppWindowReturnsToStartBoundsAndPosition(NON_RESIZABLE_APP) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val MAXIMIZE_APP = + FlickerConfigEntry( + scenarioId = ScenarioId("MAXIMIZE_APP"), + extractor = + TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = false) + ) + .build(), + assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + + listOf( + AppLayerIncreasesInSize(DESKTOP_MODE_APP), + AppWindowHasMaxDisplayHeight(DESKTOP_MODE_APP), + AppWindowHasMaxDisplayWidth(DESKTOP_MODE_APP) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val MAXIMIZE_APP_NON_RESIZABLE = + FlickerConfigEntry( + scenarioId = ScenarioId("MAXIMIZE_APP_NON_RESIZABLE"), + extractor = + TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = false) + ) + .build(), + assertions = + AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + + listOf( + AppLayerIncreasesInSize(DESKTOP_MODE_APP), + AppWindowMaintainsAspectRatioAlways(DESKTOP_MODE_APP), + AppWindowHasMaxBoundsInOnlyOneDimension(DESKTOP_MODE_APP) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val CASCADE_APP = + FlickerConfigEntry( + scenarioId = ScenarioId("CASCADE_APP"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return transitions.filter { it.type == TransitionType.OPEN } + } + } + ), + assertions = + listOf( + AppWindowInsideDisplayBoundsAtEnd(DESKTOP_MODE_APP), + AppWindowOnTopAtEnd(DESKTOP_MODE_APP), + AppWindowBecomesVisible(DESKTOP_MODE_APP), + AppWindowAlignsWithOnlyOneDisplayCornerAtEnd(DESKTOP_MODE_APP) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/EnterDesktopWithDragLandscape.kt index 9dfafe958b0b..f7b25565271f 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/EnterDesktopWithDragLandscape.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.desktopmode.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,8 +23,8 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.END_DRAG_TO_DESKTOP -import com.android.wm.shell.flicker.service.desktopmode.scenarios.EnterDesktopWithDrag +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.END_DRAG_TO_DESKTOP +import com.android.wm.shell.scenarios.EnterDesktopWithDrag import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/EnterDesktopWithDragPortrait.kt index 1c7d6237eb8a..f4bf0f97b042 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/EnterDesktopWithDragPortrait.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.desktopmode.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,8 +23,8 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.END_DRAG_TO_DESKTOP -import com.android.wm.shell.flicker.service.desktopmode.scenarios.EnterDesktopWithDrag +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.END_DRAG_TO_DESKTOP +import com.android.wm.shell.scenarios.EnterDesktopWithDrag import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppLandscape.kt new file mode 100644 index 000000000000..217956671554 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppLandscape.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.Rotation.ROTATION_90 +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MAXIMIZE_APP +import com.android.wm.shell.scenarios.MaximizeAppWindow +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Maximize app window by pressing the maximize button on the app header. + * + * Assert that the app window keeps the same increases in size, filling the vertical and horizontal + * stable display bounds. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MaximizeAppLandscape : MaximizeAppWindow(rotation = ROTATION_90) { + @ExpectedScenarios(["MAXIMIZE_APP"]) + @Test + override fun maximizeAppWindow() = super.maximizeAppWindow() + + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(MAXIMIZE_APP) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppNonResizableLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppNonResizableLandscape.kt new file mode 100644 index 000000000000..b173a60132b2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppNonResizableLandscape.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.Rotation.ROTATION_90 +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MAXIMIZE_APP_NON_RESIZABLE +import com.android.wm.shell.scenarios.MaximizeAppWindow +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Maximize non-resizable app window by pressing the maximize button on the app header. + * + * Assert that the app window keeps the same increases in size, maintaining its aspect ratio, until + * filling the vertical or horizontal stable display bounds. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MaximizeAppNonResizableLandscape : MaximizeAppWindow( + rotation = ROTATION_90, + isResizable = false +) { + @ExpectedScenarios(["MAXIMIZE_APP_NON_RESIZABLE"]) + @Test + override fun maximizeAppWindow() = super.maximizeAppWindow() + + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(MAXIMIZE_APP_NON_RESIZABLE) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppNonResizablePortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppNonResizablePortrait.kt new file mode 100644 index 000000000000..88888eee8378 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppNonResizablePortrait.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MAXIMIZE_APP_NON_RESIZABLE +import com.android.wm.shell.scenarios.MaximizeAppWindow +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Maximize non-resizable app window by pressing the maximize button on the app header. + * + * Assert that the app window keeps the same increases in size, maintaining its aspect ratio, until + * filling the vertical or horizontal stable display bounds. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MaximizeAppNonResizablePortrait : MaximizeAppWindow(isResizable = false) { + @ExpectedScenarios(["MAXIMIZE_APP_NON_RESIZABLE"]) + @Test + override fun maximizeAppWindow() = super.maximizeAppWindow() + + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(MAXIMIZE_APP_NON_RESIZABLE) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppPortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppPortrait.kt new file mode 100644 index 000000000000..b79fd203fe1e --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppPortrait.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MAXIMIZE_APP +import com.android.wm.shell.scenarios.MaximizeAppWindow +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Maximize app window by pressing the maximize button on the app header. + * + * Assert that the app window keeps the same increases in size, filling the vertical and horizontal + * stable display bounds. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MaximizeAppPortrait : MaximizeAppWindow() { + @ExpectedScenarios(["MAXIMIZE_APP"]) + @Test + override fun maximizeAppWindow() = super.maximizeAppWindow() + + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(MAXIMIZE_APP) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppsInDesktopModeLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppsInDesktopModeLandscape.kt new file mode 100644 index 000000000000..a07fa99f655f --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppsInDesktopModeLandscape.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.Rotation.ROTATION_90 +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CASCADE_APP +import com.android.wm.shell.scenarios.OpenAppsInDesktopMode +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class OpenAppsInDesktopModeLandscape : OpenAppsInDesktopMode(rotation = ROTATION_90) { + @ExpectedScenarios(["CASCADE_APP"]) + @Test + override fun openApps() = super.openApps() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CASCADE_APP) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppsInDesktopModePortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppsInDesktopModePortrait.kt new file mode 100644 index 000000000000..c7a958aa7ce3 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppsInDesktopModePortrait.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CASCADE_APP +import com.android.wm.shell.scenarios.OpenAppsInDesktopMode +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class OpenAppsInDesktopModePortrait : OpenAppsInDesktopMode() { + @ExpectedScenarios(["CASCADE_APP"]) + @Test + override fun openApps() = super.openApps() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CASCADE_APP) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppToMaximumWindowSizeLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppToMaximumWindowSizeLandscape.kt new file mode 100644 index 000000000000..0b98ba2a9cd4 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppToMaximumWindowSizeLandscape.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.Rotation +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CORNER_RESIZE_TO_MAXIMUM_SIZE +import com.android.wm.shell.scenarios.ResizeAppWithCornerResize +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Resize app window using corner resize to the greatest possible height and width in + * landscape mode. + * + * Assert that the maximum window size constraint is maintained. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ResizeAppToMaximumWindowSizeLandscape : ResizeAppWithCornerResize( + rotation = Rotation.ROTATION_90 +) { + @ExpectedScenarios(["CORNER_RESIZE_TO_MAXIMUM_SIZE"]) + @Test + override fun resizeAppWithCornerResizeToMaximumSize() = + super.resizeAppWithCornerResizeToMaximumSize() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CORNER_RESIZE_TO_MAXIMUM_SIZE) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppToMaximumWindowSizePortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppToMaximumWindowSizePortrait.kt new file mode 100644 index 000000000000..b1c04d38a46c --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppToMaximumWindowSizePortrait.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CORNER_RESIZE_TO_MAXIMUM_SIZE +import com.android.wm.shell.scenarios.ResizeAppWithCornerResize +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Resize app window using corner resize to the greatest possible height and width in + * portrait mode. + * + * Assert that the maximum window size constraint is maintained. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ResizeAppToMaximumWindowSizePortrait : ResizeAppWithCornerResize() { + @ExpectedScenarios(["CORNER_RESIZE_TO_MAXIMUM_SIZE"]) + @Test + override fun resizeAppWithCornerResizeToMaximumSize() = + super.resizeAppWithCornerResizeToMaximumSize() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CORNER_RESIZE_TO_MAXIMUM_SIZE) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppToMinimumWindowSizeLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppToMinimumWindowSizeLandscape.kt new file mode 100644 index 000000000000..45e5fdc28b0e --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppToMinimumWindowSizeLandscape.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.Rotation +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CORNER_RESIZE_TO_MINIMUM_SIZE +import com.android.wm.shell.scenarios.ResizeAppWithCornerResize +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Resize app window using corner resize to the smallest possible height and width in + * landscape mode. + * + * Assert that the minimum window size constraint is maintained. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ResizeAppToMinimumWindowSizeLandscape : ResizeAppWithCornerResize( + rotation = Rotation.ROTATION_90, + horizontalChange = -1500, + verticalChange = 1500) { + @ExpectedScenarios(["CORNER_RESIZE_TO_MINIMUM_SIZE"]) + @Test + override fun resizeAppWithCornerResize() = super.resizeAppWithCornerResize() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CORNER_RESIZE_TO_MINIMUM_SIZE) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppToMinimumWindowSizePortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppToMinimumWindowSizePortrait.kt new file mode 100644 index 000000000000..62a2571a2804 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppToMinimumWindowSizePortrait.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CORNER_RESIZE_TO_MINIMUM_SIZE +import com.android.wm.shell.scenarios.ResizeAppWithCornerResize +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Resize app window using corner resize to the smallest possible height and width in portrait mode. + * + * Assert that the minimum window size constraint is maintained. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ResizeAppToMinimumWindowSizePortrait : ResizeAppWithCornerResize(horizontalChange = -1500, + verticalChange = 1500) { + @ExpectedScenarios(["CORNER_RESIZE_TO_MINIMUM_SIZE"]) + @Test + override fun resizeAppWithCornerResize() = super.resizeAppWithCornerResize() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CORNER_RESIZE_TO_MINIMUM_SIZE) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizeLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithCornerResizeLandscape.kt index 8d1a53021683..ea8b10b28855 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizeLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithCornerResizeLandscape.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.desktopmode.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,8 +23,8 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CORNER_RESIZE -import com.android.wm.shell.flicker.service.desktopmode.scenarios.ResizeAppWithCornerResize +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CORNER_RESIZE +import com.android.wm.shell.scenarios.ResizeAppWithCornerResize import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizePortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithCornerResizePortrait.kt index 2d81c8c44799..d7bba6ec49d2 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizePortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithCornerResizePortrait.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.desktopmode.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,8 +23,8 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CORNER_RESIZE -import com.android.wm.shell.flicker.service.desktopmode.scenarios.ResizeAppWithCornerResize +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CORNER_RESIZE +import com.android.wm.shell.scenarios.ResizeAppWithCornerResize import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithEdgeResizeMouse.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithEdgeResizeMouse.kt new file mode 100644 index 000000000000..c3abf238dc0d --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithEdgeResizeMouse.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.server.wm.flicker.helpers.MotionEventHelper.InputMethod +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.EDGE_RESIZE +import com.android.wm.shell.scenarios.ResizeAppWithEdgeResize +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ResizeAppWithEdgeResizeMouse : ResizeAppWithEdgeResize(InputMethod.MOUSE) { + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeRight() = super.resizeAppWithEdgeResizeRight() + + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeLeft() = super.resizeAppWithEdgeResizeLeft() + + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeTop() = super.resizeAppWithEdgeResizeTop() + + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeBottom() = super.resizeAppWithEdgeResizeBottom() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(EDGE_RESIZE) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithEdgeResizeStylus.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithEdgeResizeStylus.kt new file mode 100644 index 000000000000..86b0e6f17b24 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithEdgeResizeStylus.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.server.wm.flicker.helpers.MotionEventHelper.InputMethod +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.EDGE_RESIZE +import com.android.wm.shell.scenarios.ResizeAppWithEdgeResize +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ResizeAppWithEdgeResizeStylus : ResizeAppWithEdgeResize(InputMethod.STYLUS) { + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeRight() = super.resizeAppWithEdgeResizeRight() + + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeLeft() = super.resizeAppWithEdgeResizeLeft() + + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeTop() = super.resizeAppWithEdgeResizeTop() + + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeBottom() = super.resizeAppWithEdgeResizeBottom() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(EDGE_RESIZE) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithEdgeResizeTouchpad.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithEdgeResizeTouchpad.kt new file mode 100644 index 000000000000..e6bb9eff6715 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithEdgeResizeTouchpad.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.server.wm.flicker.helpers.MotionEventHelper.InputMethod +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.EDGE_RESIZE +import com.android.wm.shell.scenarios.ResizeAppWithEdgeResize +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ResizeAppWithEdgeResizeTouchpad : ResizeAppWithEdgeResize(InputMethod.TOUCHPAD) { + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeRight() = super.resizeAppWithEdgeResizeRight() + + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeLeft() = super.resizeAppWithEdgeResizeLeft() + + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeTop() = super.resizeAppWithEdgeResizeTop() + + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeBottom() = super.resizeAppWithEdgeResizeBottom() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(EDGE_RESIZE) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowLeftWithButton.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowLeftWithButton.kt new file mode 100644 index 000000000000..b5090086f129 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowLeftWithButton.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.SNAP_RESIZE_LEFT_WITH_BUTTON +import com.android.wm.shell.scenarios.SnapResizeAppWindowWithButton +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Snap resize app window using the Snap Left button from the maximize menu. + * + * Assert that the app window fills the left half the display after being snap resized. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class SnapResizeAppWindowLeftWithButton : SnapResizeAppWindowWithButton(toLeft = true) { + @ExpectedScenarios(["SNAP_RESIZE_LEFT_WITH_BUTTON"]) + @Test + override fun snapResizeAppWindowWithButton() = super.snapResizeAppWindowWithButton() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(SNAP_RESIZE_LEFT_WITH_BUTTON) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowLeftWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowLeftWithDrag.kt new file mode 100644 index 000000000000..a22e7603bf0f --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowLeftWithDrag.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.SNAP_RESIZE_LEFT_WITH_DRAG +import com.android.wm.shell.scenarios.SnapResizeAppWindowWithDrag +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Snap resize app window by dragging it to the left edge of the screen. + * + * Assert that the app window fills the left half the display after being snap resized. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class SnapResizeAppWindowLeftWithDrag : SnapResizeAppWindowWithDrag(toLeft = true) { + @ExpectedScenarios(["SNAP_RESIZE_LEFT_WITH_DRAG"]) + @Test + override fun snapResizeAppWindowWithDrag() = super.snapResizeAppWindowWithDrag() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(SNAP_RESIZE_LEFT_WITH_DRAG) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowRightWithButton.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowRightWithButton.kt new file mode 100644 index 000000000000..375a2b8a61ac --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowRightWithButton.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.SNAP_RESIZE_RIGHT_WITH_BUTTON +import com.android.wm.shell.scenarios.SnapResizeAppWindowWithButton +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Snap resize app window using the Snap Right button from the maximize menu. + * + * Assert that the app window fills the right half the display after being snap resized. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class SnapResizeAppWindowRightWithButton : SnapResizeAppWindowWithButton(toLeft = false) { + @ExpectedScenarios(["SNAP_RESIZE_RIGHT_WITH_BUTTON"]) + @Test + override fun snapResizeAppWindowWithButton() = super.snapResizeAppWindowWithButton() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(SNAP_RESIZE_RIGHT_WITH_BUTTON) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowRightWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowRightWithDrag.kt new file mode 100644 index 000000000000..4a9daf7e2ea1 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeAppWindowRightWithDrag.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.SNAP_RESIZE_RIGHT_WITH_DRAG +import com.android.wm.shell.scenarios.SnapResizeAppWindowWithDrag +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Snap resize app window by dragging it to the right edge of the screen. + * + * Assert that the app window fills the right half the display after being snap resized. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class SnapResizeAppWindowRightWithDrag : SnapResizeAppWindowWithDrag(toLeft = false) { + @ExpectedScenarios(["SNAP_RESIZE_RIGHT_WITH_DRAG"]) + @Test + override fun snapResizeAppWindowWithDrag() = super.snapResizeAppWindowWithDrag() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(SNAP_RESIZE_RIGHT_WITH_DRAG) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeNonResizableAppWindowLeftWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeNonResizableAppWindowLeftWithDrag.kt new file mode 100644 index 000000000000..582658f8cb11 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeNonResizableAppWindowLeftWithDrag.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE +import com.android.wm.shell.scenarios.SnapResizeAppWindowWithDrag +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Snap resize non-resizable app window by dragging it to the left edge of the screen. + * + * Assert that the app window keeps the same size and returns to its original pre-drag position. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class SnapResizeNonResizableAppWindowLeftWithDrag : + SnapResizeAppWindowWithDrag(toLeft = true, isResizable = false) { + @ExpectedScenarios(["SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE"]) + @Test + override fun snapResizeAppWindowWithDrag() = super.snapResizeAppWindowWithDrag() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT) + .use(SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeNonResizableAppWindowRightWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeNonResizableAppWindowRightWithDrag.kt new file mode 100644 index 000000000000..7205ec412c99 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeNonResizableAppWindowRightWithDrag.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 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 + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE +import com.android.wm.shell.scenarios.SnapResizeAppWindowWithDrag +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Snap resize non-resizable app window by dragging it to the right edge of the screen. + * + * Assert that the app window keeps the same size and returns to its original pre-drag position. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class SnapResizeNonResizableAppWindowRightWithDrag : + SnapResizeAppWindowWithDrag(toLeft = false, isResizable = false) { + @ExpectedScenarios(["SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE"]) + @Test + override fun snapResizeAppWindowWithDrag() = super.snapResizeAppWindowWithDrag() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT) + .use(SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/service/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/trace_config/trace_config.textproto index 9f2e49755fec..9f2e49755fec 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/trace_config/trace_config.textproto +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/trace_config/trace_config.textproto diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/Android.bp b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/Android.bp new file mode 100644 index 000000000000..4389f09b0e5d --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/Android.bp @@ -0,0 +1,46 @@ +// +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library { + name: "WMShellScenariosDesktopMode", + platform_apis: true, + optimize: { + enabled: false, + }, + srcs: ["src/**/*.kt"], + static_libs: [ + "WMShellFlickerTestsBase", + "WMShellTestUtils", + "wm-shell-flicker-utils", + "androidx.test.ext.junit", + "flickertestapplib", + "flickerlib-helpers", + "flickerlib-trace_processor_shell", + "platform-test-annotations", + "wm-flicker-common-app-helpers", + "launcher-helper-lib", + "launcher-aosp-tapl", + ], +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/CloseAllAppsWithAppHeaderExitTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/CloseAllAppsWithAppHeaderExitTest.kt new file mode 100644 index 000000000000..a4dc52beb85d --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/CloseAllAppsWithAppHeaderExitTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.CloseAllAppsWithAppHeaderExit +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [CloseAllAppsWithAppHeaderExit]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class CloseAllAppsWithAppHeaderExitTest() : CloseAllAppsWithAppHeaderExit() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/EnterDesktopWithDragTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/EnterDesktopWithDragTest.kt new file mode 100644 index 000000000000..3d95f97c09ef --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/EnterDesktopWithDragTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.EnterDesktopWithDrag +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [EnterDesktopWithDrag]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class EnterDesktopWithDragTest : EnterDesktopWithDrag() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ExitDesktopWithDragToTopDragZoneTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ExitDesktopWithDragToTopDragZoneTest.kt new file mode 100644 index 000000000000..140c5ec15812 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ExitDesktopWithDragToTopDragZoneTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.ExitDesktopWithDragToTopDragZone +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [ExitDesktopWithDragToTopDragZone]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class ExitDesktopWithDragToTopDragZoneTest : ExitDesktopWithDragToTopDragZone() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/MaximizeAppWindowTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/MaximizeAppWindowTest.kt new file mode 100644 index 000000000000..3d3dcd09cc63 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/MaximizeAppWindowTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.MaximizeAppWindow +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [MaximizeAppWindow]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class MaximizeAppWindowTest : MaximizeAppWindow() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/OpenAppsInDesktopModeTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/OpenAppsInDesktopModeTest.kt new file mode 100644 index 000000000000..263e89f69e5a --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/OpenAppsInDesktopModeTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.OpenAppsInDesktopMode +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [OpenAppsInDesktopMode]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class OpenAppsInDesktopModeTest : OpenAppsInDesktopMode() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppCornerMultiWindowAndPipTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppCornerMultiWindowAndPipTest.kt new file mode 100644 index 000000000000..13f4775c1074 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppCornerMultiWindowAndPipTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.ResizeAppCornerMultiWindowAndPip +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [ResizeAppCornerMultiWindowAndPip]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class ResizeAppCornerMultiWindowAndPipTest : ResizeAppCornerMultiWindowAndPip() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppCornerMultiWindowTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppCornerMultiWindowTest.kt new file mode 100644 index 000000000000..bc9bb41bf320 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppCornerMultiWindowTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.ResizeAppCornerMultiWindow +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [ResizeAppCornerMultiWindow]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class ResizeAppCornerMultiWindowTest : ResizeAppCornerMultiWindow() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppWithCornerResizeTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppWithCornerResizeTest.kt new file mode 100644 index 000000000000..46168eb7c002 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppWithCornerResizeTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.ResizeAppWithCornerResize +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [ResizeAppWithCornerResize]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class ResizeAppWithCornerResizeTest : ResizeAppWithCornerResize() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppWithEdgeResizeTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppWithEdgeResizeTest.kt new file mode 100644 index 000000000000..ee2420021339 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppWithEdgeResizeTest.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.server.wm.flicker.helpers.MotionEventHelper +import com.android.wm.shell.scenarios.ResizeAppWithEdgeResize +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [ResizeAppWithEdgeResize]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class ResizeAppWithEdgeResizeTest : + ResizeAppWithEdgeResize(MotionEventHelper.InputMethod.TOUCHPAD) diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/SnapResizeAppWindowWithButtonTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/SnapResizeAppWindowWithButtonTest.kt new file mode 100644 index 000000000000..38e85c755481 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/SnapResizeAppWindowWithButtonTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.SnapResizeAppWindowWithButton +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [SnapResizeAppWindowWithButton]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class SnapResizeAppWindowWithButtonTest : SnapResizeAppWindowWithButton() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/SnapResizeAppWindowWithDragTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/SnapResizeAppWindowWithDragTest.kt new file mode 100644 index 000000000000..082a3fb0e171 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/SnapResizeAppWindowWithDragTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.SnapResizeAppWindowWithDrag +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [SnapResizeAppWindowWithDrag]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class SnapResizeAppWindowWithDragTest : SnapResizeAppWindowWithDrag() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/SwitchToOverviewFromDesktopTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/SwitchToOverviewFromDesktopTest.kt new file mode 100644 index 000000000000..fdd0d8144130 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/SwitchToOverviewFromDesktopTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.SwitchToOverviewFromDesktop +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [SwitchToOverviewFromDesktop]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class SwitchToOverviewFromDesktopTest : SwitchToOverviewFromDesktop() diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/CloseAllAppsWithAppHeaderExit.kt index e77a45729124..351a70094654 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/CloseAllAppsWithAppHeaderExit.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.desktopmode.scenarios +package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar @@ -28,7 +28,7 @@ import com.android.server.wm.flicker.helpers.MailAppHelper import com.android.server.wm.flicker.helpers.NonResizeableAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.window.flags.Flags -import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.Utils import org.junit.After import org.junit.Assume import org.junit.Before @@ -38,7 +38,6 @@ import org.junit.Test @Ignore("Base Test Class") abstract class CloseAllAppsWithAppHeaderExit -@JvmOverloads constructor(val rotation: Rotation = Rotation.ROTATION_0) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DesktopScenarioCustomAppTestBase.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DesktopScenarioCustomAppTestBase.kt new file mode 100644 index 000000000000..5a69b27d9fbb --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DesktopScenarioCustomAppTestBase.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.app.Instrumentation +import android.tools.traces.parsers.WindowManagerStateHelper +import android.tools.traces.parsers.toFlickerComponent +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.LetterboxAppHelper +import com.android.server.wm.flicker.helpers.NonResizeableAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.server.wm.flicker.testapp.ActivityOptions +import org.junit.Ignore + +/** Base test class for desktop CUJ with customizable test app. */ +@Ignore("Base Test Class") +abstract class DesktopScenarioCustomAppTestBase( + isResizeable: Boolean = true, + isLandscapeApp: Boolean = true +) { + val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + val tapl = LauncherInstrumentation() + val wmHelper = WindowManagerStateHelper(instrumentation) + val device = UiDevice.getInstance(instrumentation) + // TODO(b/363181411): Consolidate in LetterboxAppHelper. + val testApp = when { + isResizeable && isLandscapeApp -> SimpleAppHelper(instrumentation) + isResizeable && !isLandscapeApp -> SimpleAppHelper( + instrumentation, + launcherName = ActivityOptions.PortraitOnlyActivity.LABEL, + component = ActivityOptions.PortraitOnlyActivity.COMPONENT.toFlickerComponent() + ) + // NonResizeablAppHelper has no fixed orientation. + !isResizeable && isLandscapeApp -> NonResizeableAppHelper(instrumentation) + // Opens NonResizeablePortraitActivity. + else -> LetterboxAppHelper(instrumentation) + }.let { DesktopModeAppHelper(it) } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindow.kt new file mode 100644 index 000000000000..3f9927f1fab6 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindow.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 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.scenarios + +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.helpers.MailAppHelper +import com.android.server.wm.flicker.helpers.NewTasksAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Test + +@Ignore("Test Base Class") +abstract class DragAppWindowMultiWindow : DragAppWindowScenarioTestBase() +{ + private val imeAppHelper = ImeAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation)) + private val newTasksApp = DesktopModeAppHelper(NewTasksAppHelper(instrumentation)) + private val imeApp = DesktopModeAppHelper(ImeAppHelper(instrumentation)) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + testApp.enterDesktopWithDrag(wmHelper, device) + mailApp.launchViaIntent(wmHelper) + newTasksApp.launchViaIntent(wmHelper) + imeApp.launchViaIntent(wmHelper) + } + + @Test + override fun dragAppWindow() { + val (startXIme, startYIme) = getWindowDragStartCoordinate(imeAppHelper) + + imeApp.dragWindow(startXIme, startYIme, + endX = startXIme + 150, endY = startYIme + 150, + wmHelper, device) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + mailApp.exit(wmHelper) + newTasksApp.exit(wmHelper) + imeApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindowAndPip.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindowAndPip.kt new file mode 100644 index 000000000000..6d52a11153d9 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindowAndPip.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.platform.test.annotations.Postsubmit +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.helpers.MailAppHelper +import com.android.server.wm.flicker.helpers.NewTasksAppHelper +import com.android.server.wm.flicker.helpers.PipAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class DragAppWindowMultiWindowAndPip : DragAppWindowScenarioTestBase() +{ + private val imeAppHelper = ImeAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val pipApp = PipAppHelper(instrumentation) + private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation)) + private val newTasksApp = DesktopModeAppHelper(NewTasksAppHelper(instrumentation)) + private val imeApp = DesktopModeAppHelper(ImeAppHelper(instrumentation)) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + // Set string extra to ensure the app is on PiP mode at launch + pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = mapOf("enter_pip" to "true")) + testApp.enterDesktopWithDrag(wmHelper, device) + mailApp.launchViaIntent(wmHelper) + newTasksApp.launchViaIntent(wmHelper) + imeApp.launchViaIntent(wmHelper) + } + + @Test + override fun dragAppWindow() { + val (startXIme, startYIme) = getWindowDragStartCoordinate(imeAppHelper) + + imeApp.dragWindow(startXIme, startYIme, + endX = startXIme + 150, endY = startYIme + 150, + wmHelper, device) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + pipApp.exit(wmHelper) + mailApp.exit(wmHelper) + newTasksApp.exit(wmHelper) + imeApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowScenarioTestBase.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowScenarioTestBase.kt new file mode 100644 index 000000000000..7219287d97d3 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowScenarioTestBase.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +import android.tools.device.apphelpers.StandardAppHelper +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.wm.shell.Utils +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +/** Base test class for window drag CUJ. */ +@Ignore("Base Test Class") +abstract class DragAppWindowScenarioTestBase { + + val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + val tapl = LauncherInstrumentation() + val wmHelper = WindowManagerStateHelper(instrumentation) + val device = UiDevice.getInstance(instrumentation) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, Rotation.ROTATION_0) + + @Test abstract fun dragAppWindow() + + /** Return the top-center coordinate of the app header as the start coordinate. */ + fun getWindowDragStartCoordinate(appHelper: StandardAppHelper): Pair<Int, Int> { + val windowRect = wmHelper.getWindowRegion(appHelper).bounds + // Set start x-coordinate as center of app header. + val startX = windowRect.centerX() + val startY = windowRect.top + return Pair(startX, startY) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowSingleWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowSingleWindow.kt new file mode 100644 index 000000000000..91cfd17340fc --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowSingleWindow.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.platform.test.annotations.Postsubmit +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class DragAppWindowSingleWindow : DragAppWindowScenarioTestBase() +{ + private val simpleAppHelper = SimpleAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(simpleAppHelper) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + override fun dragAppWindow() { + val (startXTest, startYTest) = getWindowDragStartCoordinate(simpleAppHelper) + testApp.dragWindow(startXTest, startYTest, + endX = startXTest + 150, endY = startYTest + 150, + wmHelper, device) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithAppHandleMenu.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithAppHandleMenu.kt new file mode 100644 index 000000000000..107305044d55 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithAppHandleMenu.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.platform.test.annotations.Postsubmit +import android.app.Instrumentation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class EnterDesktopWithAppHandleMenu { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val simpleAppHelper = SimpleAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(simpleAppHelper) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + } + + @Test + open fun enterDesktopWithAppHandleMenu() { + simpleAppHelper.launchViaIntent(wmHelper) + testApp.enterDesktopModeFromAppHandleMenu(wmHelper, device) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt new file mode 100644 index 000000000000..967bd29958c2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.tools.NavBar +import android.tools.Rotation +import android.tools.flicker.rules.ChangeDisplayOrientationRule +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +@Ignore("Test Base Class") +abstract class EnterDesktopWithDrag +constructor( + val rotation: Rotation = Rotation.ROTATION_0, + isResizeable: Boolean = true, + isLandscapeApp: Boolean = true, +) : DesktopScenarioCustomAppTestBase(isResizeable, isLandscapeApp) { + + @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + ChangeDisplayOrientationRule.setRotation(rotation) + tapl.enableTransientTaskbar(false) + } + + @Test + open fun enterDesktopWithDrag() { + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt new file mode 100644 index 000000000000..824c4482c1e6 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.tools.NavBar +import android.tools.Rotation +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +@Ignore("Test Base Class") +abstract class ExitDesktopWithDragToTopDragZone +constructor( + val rotation: Rotation = Rotation.ROTATION_0, + isResizeable: Boolean = true, + isLandscapeApp: Boolean = true, +) : DesktopScenarioCustomAppTestBase(isResizeable, isLandscapeApp) { + + @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + open fun exitDesktopWithDragToTopDragZone() { + testApp.exitDesktopWithDragToTopDragZone(wmHelper, device) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ResizeAppWithCornerResize.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt index ac9089a5c1bd..a54d497bf511 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ResizeAppWithCornerResize.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt @@ -14,19 +14,21 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.desktopmode.scenarios +package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation +import android.tools.flicker.rules.ChangeDisplayOrientationRule import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.NonResizeableAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.window.flags.Flags -import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.Utils import org.junit.After import org.junit.Assume import org.junit.Before @@ -34,17 +36,19 @@ import org.junit.Ignore import org.junit.Rule import org.junit.Test - -@Ignore("Base Test Class") -abstract class ResizeAppWithCornerResize -@JvmOverloads -constructor(val rotation: Rotation = Rotation.ROTATION_0) { +@Ignore("Test Base Class") +abstract class MaximizeAppWindow +constructor(private val rotation: Rotation = Rotation.ROTATION_0, isResizable: Boolean = true) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() private val tapl = LauncherInstrumentation() private val wmHelper = WindowManagerStateHelper(instrumentation) private val device = UiDevice.getInstance(instrumentation) - private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val testApp = if (isResizable) { + DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + } else { + DesktopModeAppHelper(NonResizeableAppHelper(instrumentation)) + } @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) @@ -53,12 +57,13 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) + ChangeDisplayOrientationRule.setRotation(rotation) testApp.enterDesktopWithDrag(wmHelper, device) } @Test - open fun resizeAppWithCornerResize() { - testApp.cornerResize(wmHelper, device, DesktopModeAppHelper.Corners.RIGHT_TOP, 50, -50) + open fun maximizeAppWindow() { + testApp.maximiseDesktopApp(wmHelper, device) } @After diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeWindowOnAppOpen.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeWindowOnAppOpen.kt new file mode 100644 index 000000000000..b86765e23422 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeWindowOnAppOpen.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.platform.test.annotations.Postsubmit +import android.app.Instrumentation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.helpers.LetterboxAppHelper +import com.android.server.wm.flicker.helpers.MailAppHelper +import com.android.server.wm.flicker.helpers.NewTasksAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner +/** + * Base scenario test for minimizing the least recently used window when a new window is opened + * above the window limit. For tangor devices, which this test currently runs on, the window limit + * is 4. + */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class MinimizeWindowOnAppOpen() +{ + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation)) + private val newTasksApp = DesktopModeAppHelper(NewTasksAppHelper(instrumentation)) + private val imeApp = DesktopModeAppHelper(ImeAppHelper(instrumentation)) + private val letterboxAppHelper = DesktopModeAppHelper(LetterboxAppHelper(instrumentation)) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + testApp.enterDesktopWithDrag(wmHelper, device) + mailApp.launchViaIntent(wmHelper) + newTasksApp.launchViaIntent(wmHelper) + imeApp.launchViaIntent(wmHelper) + } + + @Test + open fun openAppToMinimizeWindow() { + // Launch a new app while 4 apps are already open on desktop. This should result in the + // first app we opened to be minimized. + letterboxAppHelper.launchViaIntent(wmHelper) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + mailApp.exit(wmHelper) + newTasksApp.exit(wmHelper) + imeApp.exit(wmHelper) + letterboxAppHelper.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppsInDesktopMode.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppsInDesktopMode.kt new file mode 100644 index 000000000000..aad266fb8374 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppsInDesktopMode.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.app.Instrumentation +import android.tools.flicker.rules.ChangeDisplayOrientationRule +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.helpers.MailAppHelper +import com.android.server.wm.flicker.helpers.NewTasksAppHelper +import com.android.server.wm.flicker.helpers.NonResizeableAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +@Ignore("Test Base Class") +abstract class OpenAppsInDesktopMode(val rotation: Rotation = Rotation.ROTATION_0) { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val firstApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val secondApp = MailAppHelper(instrumentation) + private val thirdApp = NewTasksAppHelper(instrumentation) + private val fourthApp = ImeAppHelper(instrumentation) + private val fifthApp = NonResizeableAppHelper(instrumentation) + + @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_3BUTTON, rotation) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + tapl.enableTransientTaskbar(false) + ChangeDisplayOrientationRule.setRotation(rotation) + firstApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + open fun openApps() { + secondApp.launchViaIntent(wmHelper) + thirdApp.launchViaIntent(wmHelper) + fourthApp.launchViaIntent(wmHelper) + fifthApp.launchViaIntent(wmHelper) + } + + @After + fun teardown() { + fifthApp.exit(wmHelper) + fourthApp.exit(wmHelper) + thirdApp.exit(wmHelper) + secondApp.exit(wmHelper) + firstApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt new file mode 100644 index 000000000000..bfee3181cbc0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.helpers.MailAppHelper +import com.android.server.wm.flicker.helpers.NewTasksAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +@Ignore("Test Base Class") +abstract class ResizeAppCornerMultiWindow +constructor(val rotation: Rotation = Rotation.ROTATION_0, + val horizontalChange: Int = 50, + val verticalChange: Int = -50) { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation)) + private val newTasksApp = DesktopModeAppHelper(NewTasksAppHelper(instrumentation)) + private val imeApp = DesktopModeAppHelper(ImeAppHelper(instrumentation)) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + testApp.enterDesktopWithDrag(wmHelper, device) + mailApp.launchViaIntent(wmHelper) + newTasksApp.launchViaIntent(wmHelper) + imeApp.launchViaIntent(wmHelper) + } + + @Test + open fun resizeAppWithCornerResize() { + imeApp.cornerResize(wmHelper, + device, + DesktopModeAppHelper.Corners.RIGHT_TOP, + horizontalChange, + verticalChange) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + mailApp.exit(wmHelper) + newTasksApp.exit(wmHelper) + imeApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindowAndPip.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindowAndPip.kt new file mode 100644 index 000000000000..5b1b64e7c562 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindowAndPip.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.helpers.MailAppHelper +import com.android.server.wm.flicker.helpers.NewTasksAppHelper +import com.android.server.wm.flicker.helpers.PipAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +@Ignore("Test Base Class") +abstract class ResizeAppCornerMultiWindowAndPip +constructor(val rotation: Rotation = Rotation.ROTATION_0, + val horizontalChange: Int = 50, + val verticalChange: Int = -50) { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val pipApp = PipAppHelper(instrumentation) + private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation)) + private val newTasksApp = DesktopModeAppHelper(NewTasksAppHelper(instrumentation)) + private val imeApp = DesktopModeAppHelper(ImeAppHelper(instrumentation)) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + // Set string extra to ensure the app is on PiP mode at launch + pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = mapOf("enter_pip" to "true")) + testApp.enterDesktopWithDrag(wmHelper, device) + mailApp.launchViaIntent(wmHelper) + newTasksApp.launchViaIntent(wmHelper) + imeApp.launchViaIntent(wmHelper) + } + + @Test + open fun resizeAppWithCornerResize() { + imeApp.cornerResize(wmHelper, + device, + DesktopModeAppHelper.Corners.RIGHT_TOP, + horizontalChange, + verticalChange) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + pipApp.exit(wmHelper) + mailApp.exit(wmHelper) + newTasksApp.exit(wmHelper) + imeApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt new file mode 100644 index 000000000000..bd25639466a3 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.NonResizeableAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +@Ignore("Test Base Class") +abstract class ResizeAppWithCornerResize( + val rotation: Rotation = Rotation.ROTATION_0, + val horizontalChange: Int = 200, + val verticalChange: Int = -200, + val appProperty: AppProperty = AppProperty.STANDARD +) { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = + DesktopModeAppHelper( + when (appProperty) { + AppProperty.STANDARD -> SimpleAppHelper(instrumentation) + AppProperty.NON_RESIZABLE -> NonResizeableAppHelper(instrumentation) + } + ) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + open fun resizeAppWithCornerResize() { + testApp.cornerResize( + wmHelper, + device, + DesktopModeAppHelper.Corners.RIGHT_TOP, + horizontalChange, + verticalChange + ) + } + + @Test + open fun resizeAppWithCornerResizeToMaximumSize() { + val maxResizeChange = 3000 + testApp.cornerResize( + wmHelper, + device, + DesktopModeAppHelper.Corners.RIGHT_TOP, + maxResizeChange, + -maxResizeChange + ) + testApp.cornerResize( + wmHelper, + device, + DesktopModeAppHelper.Corners.LEFT_BOTTOM, + -maxResizeChange, + maxResizeChange + ) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } + + companion object { + enum class AppProperty { + STANDARD, + NON_RESIZABLE + } + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithEdgeResize.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithEdgeResize.kt new file mode 100644 index 000000000000..67802387b267 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithEdgeResize.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.MotionEventHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +@Ignore("Test Base Class") +abstract class ResizeAppWithEdgeResize +constructor( + val inputMethod: MotionEventHelper.InputMethod, + val rotation: Rotation = Rotation.ROTATION_90 +) { + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val motionEventHelper = MotionEventHelper(instrumentation, inputMethod) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + + @Before + fun setup() { + Assume.assumeTrue( + Flags.enableDesktopWindowingMode() + && Flags.enableWindowingEdgeDragResize() && tapl.isTablet + ) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + open fun resizeAppWithEdgeResizeRight() { + testApp.edgeResize( + wmHelper, + motionEventHelper, + DesktopModeAppHelper.Edges.RIGHT + ) + } + + @Test + open fun resizeAppWithEdgeResizeLeft() { + testApp.edgeResize( + wmHelper, + motionEventHelper, + DesktopModeAppHelper.Edges.LEFT + ) + } + + @Test + open fun resizeAppWithEdgeResizeTop() { + testApp.edgeResize( + wmHelper, + motionEventHelper, + DesktopModeAppHelper.Edges.TOP + ) + } + + @Test + open fun resizeAppWithEdgeResizeBottom() { + testApp.edgeResize( + wmHelper, + motionEventHelper, + DesktopModeAppHelper.Edges.BOTTOM + ) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt new file mode 100644 index 000000000000..2b40497844ef --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.NonResizeableAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +@Ignore("Test Base Class") +abstract class SnapResizeAppWindowWithButton +constructor(private val toLeft: Boolean = true, isResizable: Boolean = true) { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = if (isResizable) { + DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + } else { + DesktopModeAppHelper(NonResizeableAppHelper(instrumentation)) + } + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, Rotation.ROTATION_0) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + open fun snapResizeAppWindowWithButton() { + testApp.snapResizeDesktopApp(wmHelper, device, instrumentation.context, toLeft) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt new file mode 100644 index 000000000000..b4bd7e1c5211 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.NonResizeableAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +@Ignore("Test Base Class") +abstract class SnapResizeAppWindowWithDrag +constructor(private val toLeft: Boolean = true, isResizable: Boolean = true) { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = if (isResizable) { + DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + } else { + DesktopModeAppHelper(NonResizeableAppHelper(instrumentation)) + } + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, Rotation.ROTATION_0) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + open fun snapResizeAppWindowWithDrag() { + testApp.dragToSnapResizeRegion(wmHelper, device, toLeft) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionResizeAndDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionResizeAndDrag.kt new file mode 100644 index 000000000000..f08e50e0d4ee --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionResizeAndDrag.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.app.Instrumentation +import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation +import android.tools.device.apphelpers.CalculatorAppHelper +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper.Corners.LEFT_TOP +import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class StartAppMediaProjectionResizeAndDrag { + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + + private val targetApp = CalculatorAppHelper(instrumentation) + private val mediaProjectionAppHelper = StartMediaProjectionAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(mediaProjectionAppHelper) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, Rotation.ROTATION_0) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(0) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + open fun startMediaProjectionAndResize() { + mediaProjectionAppHelper.startSingleAppMediaProjection(wmHelper, targetApp) + + with(DesktopModeAppHelper(targetApp)) { + val windowRect = wmHelper.getWindowRegion(this).bounds + // Set start x-coordinate as center of app header. + val startX = windowRect.centerX() + val startY = windowRect.top + + dragWindow(startX, startY, endX = startX + 150, endY = startY + 150, wmHelper, device) + cornerResize(wmHelper, device, LEFT_TOP, -200, -200) + } + } + + @After + fun teardown() { + testApp.exit(wmHelper) + targetApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithMaxDesktopWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithMaxDesktopWindows.kt new file mode 100644 index 000000000000..ce235d445fe5 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithMaxDesktopWindows.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.app.Instrumentation +import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation +import android.tools.device.apphelpers.CalculatorAppHelper +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.helpers.MailAppHelper +import com.android.server.wm.flicker.helpers.NewTasksAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class StartAppMediaProjectionWithMaxDesktopWindows { + + val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + val tapl = LauncherInstrumentation() + val wmHelper = WindowManagerStateHelper(instrumentation) + val device = UiDevice.getInstance(instrumentation) + + private val targetApp = CalculatorAppHelper(instrumentation) + private val mailApp = MailAppHelper(instrumentation) + private val newTasksApp = DesktopModeAppHelper(NewTasksAppHelper(instrumentation)) + private val imeApp = DesktopModeAppHelper(ImeAppHelper(instrumentation)) + private val simpleApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val mediaProjectionAppHelper = StartMediaProjectionAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(mediaProjectionAppHelper) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, Rotation.ROTATION_0) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(0) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + open fun startMediaProjection() { + mediaProjectionAppHelper.startSingleAppMediaProjection(wmHelper, targetApp) + mailApp.launchViaIntent(wmHelper) + simpleApp.launchViaIntent(wmHelper) + newTasksApp.launchViaIntent(wmHelper) + imeApp.launchViaIntent(wmHelper) + } + + @After + fun teardown() { + mailApp.exit(wmHelper) + simpleApp.exit(wmHelper) + newTasksApp.exit(wmHelper) + imeApp.exit(wmHelper) + targetApp.exit(wmHelper) + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithMaxDesktopWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithMaxDesktopWindows.kt new file mode 100644 index 000000000000..005195296c62 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithMaxDesktopWindows.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.app.Instrumentation +import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.helpers.MailAppHelper +import com.android.server.wm.flicker.helpers.NewTasksAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class StartScreenMediaProjectionWithMaxDesktopWindows { + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, Rotation.ROTATION_0) + + private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation)) + private val newTasksApp = DesktopModeAppHelper(NewTasksAppHelper(instrumentation)) + private val imeApp = DesktopModeAppHelper(ImeAppHelper(instrumentation)) + private val simpleApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val mediaProjectionAppHelper = StartMediaProjectionAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(mediaProjectionAppHelper) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + open fun startMediaProjection() { + mediaProjectionAppHelper.startEntireScreenMediaProjection(wmHelper) + simpleApp.launchViaIntent(wmHelper) + mailApp.launchViaIntent(wmHelper) + newTasksApp.launchViaIntent(wmHelper) + imeApp.launchViaIntent(wmHelper) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + simpleApp.exit(wmHelper) + mailApp.exit(wmHelper) + newTasksApp.exit(wmHelper) + imeApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SwitchToOverviewFromDesktop.kt index fe139d2d24a0..dad2eb633c72 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SwitchToOverviewFromDesktop.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.desktopmode.scenarios +package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar @@ -26,7 +26,7 @@ import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.helpers.DesktopModeAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.window.flags.Flags -import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.Utils import org.junit.After import org.junit.Assume import org.junit.Before @@ -34,11 +34,14 @@ import org.junit.Ignore import org.junit.Rule import org.junit.Test - +/** +* Base test for opening recent apps overview from desktop mode. +* +* Navigation mode can be passed as a constructor parameter, by default it is set to gesture navigation. +*/ @Ignore("Base Test Class") -abstract class EnterDesktopWithDrag -@JvmOverloads -constructor(val rotation: Rotation = Rotation.ROTATION_0) { +abstract class SwitchToOverviewFromDesktop +constructor(val navigationMode: NavBar = NavBar.MODE_GESTURAL) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() private val tapl = LauncherInstrumentation() @@ -46,18 +49,17 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { private val device = UiDevice.getInstance(instrumentation) private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) - @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + @Rule @JvmField val testSetupRule = Utils.testSetupRule(navigationMode, Rotation.ROTATION_0) @Before fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) - tapl.setEnableRotation(true) - tapl.setExpectedRotation(rotation.value) + testApp.enterDesktopWithDrag(wmHelper, device) } @Test - open fun enterDesktopWithDrag() { - testApp.enterDesktopWithDrag(wmHelper, device) + open fun switchToOverview() { + tapl.getLaunchedAppState().switchToOverview() } @After diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/Android.bp b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/Android.bp new file mode 100644 index 000000000000..85e6a8d1d865 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/Android.bp @@ -0,0 +1,38 @@ +// +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "WMShellFlickerTestsMediaProjection", + defaults: ["WMShellFlickerTestsDefault"], + manifest: "AndroidManifest.xml", + test_config_template: "AndroidTestTemplate.xml", + srcs: ["src/**/*.kt"], + static_libs: [ + "WMShellFlickerTestsBase", + "WMShellScenariosMediaProjection", + "WMShellTestUtils", + ], + data: ["trace_config/*"], +} diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidManifest.xml b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidManifest.xml new file mode 100644 index 000000000000..74b0daf3a2aa --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidManifest.xml @@ -0,0 +1,85 @@ +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.android.wm.shell.flicker"> + + <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/> + <!-- Read and write traces from external storage --> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + <!-- Allow the test to write directly to /sdcard/ --> + <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> + <!-- Write secure settings --> + <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> + <!-- Capture screen contents --> + <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" /> + <!-- Enable / Disable tracing !--> + <uses-permission android:name="android.permission.DUMP" /> + <!-- Run layers trace --> + <uses-permission android:name="android.permission.HARDWARE_TEST"/> + <!-- Capture screen recording --> + <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT"/> + <!-- Workaround grant runtime permission exception from b/152733071 --> + <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/> + <uses-permission android:name="android.permission.READ_LOGS"/> + <!-- Force-stop test apps --> + <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES"/> + <!-- Control test app's media session --> + <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/> + <!-- ATM.removeRootTasksWithActivityTypes() --> + <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" /> + <!-- Enable bubble notification--> + <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" /> + <!-- Allow the test to connect to perfetto trace processor --> + <uses-permission android:name="android.permission.INTERNET"/> + <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" /> + <uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" /> + + <!-- Allow the test to write directly to /sdcard/ and connect to trace processor --> + <application android:requestLegacyExternalStorage="true" + android:networkSecurityConfig="@xml/network_security_config" + android:largeHeap="true"> + <uses-library android:name="android.test.runner"/> + + <service android:name=".NotificationListener" + android:exported="true" + android:label="WMShellTestsNotificationListenerService" + android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"> + <intent-filter> + <action android:name="android.service.notification.NotificationListenerService" /> + </intent-filter> + </service> + + <service android:name="com.android.wm.shell.flicker.utils.MediaProjectionService" + android:foregroundServiceType="mediaProjection" + android:label="WMShellTestsMediaProjectionService" + android:enabled="true"> + </service> + + <!-- (b/197936012) Remove startup provider due to test timeout issue --> + <provider + android:name="androidx.startup.InitializationProvider" + android:authorities="${applicationId}.androidx-startup" + tools:node="remove" /> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.wm.shell.flicker" + android:label="WindowManager Shell Flicker Tests"> + </instrumentation> +</manifest> diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidTestTemplate.xml new file mode 100644 index 000000000000..40dbbac32c7f --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidTestTemplate.xml @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<configuration description="Runs WindowManager Shell Flicker Tests {MODULE}"> + <option name="test-tag" value="FlickerTests"/> + <!-- Needed for storing the perfetto trace files in the sdcard/test_results--> + <option name="isolated-storage" value="false"/> + + <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <!-- disable DeprecatedTargetSdk warning --> + <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> + <!-- keeps the screen on during tests --> + <option name="screen-always-on" value="on"/> + <!-- prevents the phone from restarting --> + <option name="force-skip-system-props" value="true"/> + <!-- set WM tracing verbose level to all --> + <option name="run-command" value="cmd window tracing level all"/> + <!-- set WM tracing to frame (avoid incomplete states) --> + <option name="run-command" value="cmd window tracing frame"/> + <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests --> + <option name="run-command" value="pm disable com.google.android.internal.betterbug"/> + <!-- ensure lock screen mode is swipe --> + <option name="run-command" value="locksettings set-disabled false"/> + <!-- restart launcher to activate TAPL --> + <option name="run-command" + value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher"/> + <!-- Increase trace size: 20mb for WM and 80mb for SF --> + <option name="run-command" value="cmd window tracing size 20480"/> + <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920"/> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="test-user-token" value="%TEST_USER%"/> + <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> + <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> + <option name="run-command" value="settings put system show_touches 1"/> + <option name="run-command" value="settings put system pointer_location 1"/> + <option name="teardown-command" + value="settings delete secure show_ime_with_hard_keyboard"/> + <option name="teardown-command" value="settings delete system show_touches"/> + <option name="teardown-command" value="settings delete system pointer_location"/> + <option name="teardown-command" + value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true"/> + <option name="test-file-name" value="{MODULE}.apk"/> + <option name="test-file-name" value="FlickerTestApp.apk"/> + </target_preparer> + + <!-- Needed for pushing the trace config file --> + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> + <option name="push-file" + key="trace_config.textproto" + value="/data/misc/perfetto-traces/trace_config.textproto" + /> + <!--Install the content provider automatically when we push some file in sdcard folder.--> + <!--Needed to avoid the installation during the test suite.--> + <option name="push-file" key="trace_config.textproto" value="/sdcard/sample.textproto"/> + </target_preparer> + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="{PACKAGE}"/> + <option name="shell-timeout" value="6600s"/> + <option name="test-timeout" value="6000s"/> + <option name="hidden-api-checks" value="false"/> + <option name="device-listeners" value="android.device.collectors.PerfettoListener"/> + <!-- PerfettoListener related arguments --> + <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true"/> + <option name="instrumentation-arg" + key="perfetto_config_file" + value="trace_config.textproto" + /> + <option name="instrumentation-arg" key="per_run" value="true"/> + <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/> + </test> + <!-- Needed for pulling the collected trace config on to the host --> + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="pull-pattern-keys" value="perfetto_file_path"/> + <option name="directory-keys" + value="/data/user/0/com.android.wm.shell.flicker/files"/> + <option name="collect-on-run-ended-only" value="true"/> + <option name="clean-up" value="true"/> + </metrics_collector> +</configuration> diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/res/xml/network_security_config.xml b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/res/xml/network_security_config.xml index 4bd9ca049f55..4bd9ca049f55 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/res/xml/network_security_config.xml +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/res/xml/network_security_config.xml diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/trace_config/trace_config.textproto new file mode 100644 index 000000000000..9f2e49755fec --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/trace_config/trace_config.textproto @@ -0,0 +1,71 @@ +# Copyright (C) 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# proto-message: TraceConfig + +# Enable periodic flushing of the trace buffer into the output file. +write_into_file: true + +# Writes the userspace buffer into the file every 1s. +file_write_period_ms: 2500 + +# See b/126487238 - we need to guarantee ordering of events. +flush_period_ms: 30000 + +# The trace buffers needs to be big enough to hold |file_write_period_ms| of +# trace data. The trace buffer sizing depends on the number of trace categories +# enabled and the device activity. + +# RSS events +buffers: { + size_kb: 63488 + fill_policy: RING_BUFFER +} + +data_sources { + config { + name: "linux.process_stats" + target_buffer: 0 + # polled per-process memory counters and process/thread names. + # If you don't want the polled counters, remove the "process_stats_config" + # section, but keep the data source itself as it still provides on-demand + # thread/process naming for ftrace data below. + process_stats_config { + scan_all_processes_on_start: true + } + } +} + +data_sources: { + config { + name: "linux.ftrace" + ftrace_config { + ftrace_events: "ftrace/print" + ftrace_events: "task/task_newtask" + ftrace_events: "task/task_rename" + atrace_categories: "ss" + atrace_categories: "wm" + atrace_categories: "am" + atrace_categories: "aidl" + atrace_categories: "input" + atrace_categories: "binder_driver" + atrace_categories: "sched_process_exit" + atrace_apps: "com.android.server.wm.flicker.testapp" + atrace_apps: "com.android.systemui" + atrace_apps: "com.android.wm.shell.flicker.service" + atrace_apps: "com.google.android.apps.nexuslauncher" + } + } +} + diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/Android.bp b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/Android.bp new file mode 100644 index 000000000000..997a0af68d1a --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/Android.bp @@ -0,0 +1,46 @@ +// +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library { + name: "WMShellScenariosMediaProjection", + platform_apis: true, + optimize: { + enabled: false, + }, + srcs: ["src/**/*.kt"], + static_libs: [ + "WMShellFlickerTestsBase", + "WMShellTestUtils", + "wm-shell-flicker-utils", + "androidx.test.ext.junit", + "flickertestapplib", + "flickerlib-helpers", + "flickerlib-trace_processor_shell", + "platform-test-annotations", + "wm-flicker-common-app-helpers", + "launcher-helper-lib", + "launcher-aosp-tapl", + ], +} diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithDisplayRotations.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithDisplayRotations.kt new file mode 100644 index 000000000000..f5fb4cec5535 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithDisplayRotations.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.app.Instrumentation +import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation +import android.tools.device.apphelpers.CalculatorAppHelper +import android.tools.flicker.rules.ChangeDisplayOrientationRule +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class StartAppMediaProjectionWithDisplayRotations { + + val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + val tapl = LauncherInstrumentation() + val wmHelper = WindowManagerStateHelper(instrumentation) + val device = UiDevice.getInstance(instrumentation) + + private val initialRotation = Rotation.ROTATION_0 + private val targetApp = CalculatorAppHelper(instrumentation) + private val testApp = StartMediaProjectionAppHelper(instrumentation) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, initialRotation) + + @Before + fun setup() { + tapl.setEnableRotation(true) + tapl.setExpectedRotation(initialRotation.value) + testApp.launchViaIntent(wmHelper) + } + + @Test + open fun startMediaProjectionAndRotate() { + testApp.startSingleAppMediaProjection(wmHelper, targetApp) + wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() + + ChangeDisplayOrientationRule.setRotation(Rotation.ROTATION_90) + ChangeDisplayOrientationRule.setRotation(Rotation.ROTATION_270) + ChangeDisplayOrientationRule.setRotation(Rotation.ROTATION_0) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithDisplayRotations.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithDisplayRotations.kt new file mode 100644 index 000000000000..28f3cc758c22 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithDisplayRotations.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.app.Instrumentation +import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation +import android.tools.flicker.rules.ChangeDisplayOrientationRule +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class StartScreenMediaProjectionWithDisplayRotations { + + val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + val tapl = LauncherInstrumentation() + val wmHelper = WindowManagerStateHelper(instrumentation) + val device = UiDevice.getInstance(instrumentation) + + private val initialRotation = Rotation.ROTATION_0 + private val testApp = StartMediaProjectionAppHelper(instrumentation) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, initialRotation) + + @Before + fun setup() { + tapl.setEnableRotation(true) + testApp.launchViaIntent(wmHelper) + } + + @Test + open fun startMediaProjectionAndRotate() { + testApp.startEntireScreenMediaProjection(wmHelper) + wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() + + ChangeDisplayOrientationRule.setRotation(Rotation.ROTATION_90) + ChangeDisplayOrientationRule.setRotation(Rotation.ROTATION_270) + ChangeDisplayOrientationRule.setRotation(initialRotation) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/OWNERS b/libs/WindowManager/Shell/tests/e2e/splitscreen/OWNERS index 3ab6a1ee061d..3ab6a1ee061d 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/OWNERS +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/OWNERS diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/Android.bp b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/Android.bp new file mode 100644 index 000000000000..a231e381beda --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/Android.bp @@ -0,0 +1,329 @@ +// +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + default_team: "trendy_team_multitasking_windowing", + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +filegroup { + name: "WMShellFlickerTestsSplitScreenBase-src", + srcs: [ + "src/**/benchmark/*.kt", + ], +} + +java_library { + name: "WMShellFlickerTestsSplitScreenBase", + srcs: [ + ":WMShellFlickerTestsSplitScreenBase-src", + ], + static_libs: [ + "WMShellFlickerTestsBase", + "wm-shell-flicker-utils", + "androidx.test.ext.junit", + "flickertestapplib", + "flickerlib", + "flickerlib-helpers", + "flickerlib-trace_processor_shell", + "platform-test-annotations", + "wm-flicker-common-app-helpers", + "wm-flicker-common-assertions", + "launcher-helper-lib", + "launcher-aosp-tapl", + ], +} + +android_test { + name: "WMShellFlickerTestsSplitScreen", + defaults: ["WMShellFlickerTestsDefault"], + manifest: "AndroidManifest.xml", + package_name: "com.android.wm.shell.flicker.splitscreen", + instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen", + test_config_template: "AndroidTestTemplate.xml", + srcs: ["src/**/*.kt"], + exclude_srcs: ["src/**/benchmark/*.kt"], + static_libs: [ + "WMShellFlickerTestsBase", + "WMShellFlickerTestsSplitScreenBase", + ], + data: ["trace_config/*"], +} + +//////////////////////////////////////////////////////////////////////////////// +// Begin cleanup after gcl merges + +filegroup { + name: "WMShellFlickerTestsSplitScreenGroup1-src", + srcs: [ + "src/**/A*.kt", + "src/**/B*.kt", + "src/**/C*.kt", + "src/**/D*.kt", + ], +} + +filegroup { + name: "WMShellFlickerTestsSplitScreenGroup2-src", + srcs: [ + "src/**/E*.kt", + ], +} + +filegroup { + name: "WMShellFlickerTestsSplitScreenGroup3-src", + srcs: [ + "src/**/S*.kt", + ], +} + +filegroup { + name: "WMShellFlickerTestsSplitScreenGroupOther-src", + srcs: [ + "src/**/*.kt", + ], +} + +android_test { + name: "WMShellFlickerTestsSplitScreenGroup1", + defaults: ["WMShellFlickerTestsDefault"], + manifest: "AndroidManifest.xml", + package_name: "com.android.wm.shell.flicker.splitscreen", + instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen", + test_config_template: "AndroidTestTemplate.xml", + srcs: [ + ":WMShellFlickerTestsSplitScreenGroup1-src", + ], + static_libs: [ + "WMShellFlickerTestsBase", + "WMShellFlickerTestsSplitScreenBase", + ], + data: ["trace_config/*"], +} + +android_test { + name: "WMShellFlickerTestsSplitScreenGroup2", + defaults: ["WMShellFlickerTestsDefault"], + manifest: "AndroidManifest.xml", + package_name: "com.android.wm.shell.flicker.splitscreen", + instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen", + test_config_template: "AndroidTestTemplate.xml", + srcs: [ + ":WMShellFlickerTestsSplitScreenGroup2-src", + ], + static_libs: [ + "WMShellFlickerTestsBase", + "WMShellFlickerTestsSplitScreenBase", + ], + data: ["trace_config/*"], +} + +android_test { + name: "WMShellFlickerTestsSplitScreenGroup3", + defaults: ["WMShellFlickerTestsDefault"], + manifest: "AndroidManifest.xml", + package_name: "com.android.wm.shell.flicker.splitscreen", + instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen", + test_config_template: "AndroidTestTemplate.xml", + srcs: [ + ":WMShellFlickerTestsSplitScreenGroup3-src", + ], + static_libs: [ + "WMShellFlickerTestsBase", + "WMShellFlickerTestsSplitScreenBase", + ], + data: ["trace_config/*"], +} + +android_test { + name: "WMShellFlickerTestsSplitScreenGroupOther", + defaults: ["WMShellFlickerTestsDefault"], + manifest: "AndroidManifest.xml", + package_name: "com.android.wm.shell.flicker.splitscreen", + instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen", + test_config_template: "AndroidTestTemplate.xml", + srcs: [ + ":WMShellFlickerTestsSplitScreenGroupOther-src", + ], + exclude_srcs: [ + ":WMShellFlickerTestsSplitScreenGroup1-src", + ":WMShellFlickerTestsSplitScreenGroup2-src", + ":WMShellFlickerTestsSplitScreenGroup3-src", + ], + static_libs: [ + "WMShellFlickerTestsBase", + "WMShellFlickerTestsSplitScreenBase", + ], + data: ["trace_config/*"], +} + +//////////////////////////////////////////////////////////////////////////////// +// End cleanup after gcl merges + +//////////////////////////////////////////////////////////////////////////////// +// Begin breakdowns for FlickerTestsRotation module + +test_module_config { + name: "WMShellFlickerTestsSplitScreen-CatchAll", + base: "WMShellFlickerTestsSplitScreen", + exclude_filters: [ + "com.android.wm.shell.flicker.splitscreen.CopyContentInSplit", + "com.android.wm.shell.flicker.splitscreen.DismissSplitScreenByDivider", + "com.android.wm.shell.flicker.splitscreen.DismissSplitScreenByGoHome", + "com.android.wm.shell.flicker.splitscreen.DragDividerToResize", + "com.android.wm.shell.flicker.splitscreen.EnterSplitScreenByDragFromAllApps", + "com.android.wm.shell.flicker.splitscreen.EnterSplitScreenByDragFromNotification", + "com.android.wm.shell.flicker.splitscreen.EnterSplitScreenByDragFromShortcut", + "com.android.wm.shell.flicker.splitscreen.EnterSplitScreenByDragFromTaskbar", + "com.android.wm.shell.flicker.splitscreen.EnterSplitScreenFromOverview", + "com.android.wm.shell.flicker.splitscreen.MultipleShowImeRequestsInSplitScreen", + "com.android.wm.shell.flicker.splitscreen.SwitchAppByDoubleTapDivider", + "com.android.wm.shell.flicker.splitscreen.SwitchBackToSplitFromAnotherApp", + "com.android.wm.shell.flicker.splitscreen.SwitchBackToSplitFromHome", + "com.android.wm.shell.flicker.splitscreen.SwitchBackToSplitFromRecent", + "com.android.wm.shell.flicker.splitscreen.SwitchBetweenSplitPairs", + "com.android.wm.shell.flicker.splitscreen.SwitchBetweenSplitPairsNoPip", + "com.android.wm.shell.flicker.splitscreen.", + ], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsSplitScreen-CopyContentInSplit", + base: "WMShellFlickerTestsSplitScreen", + include_filters: ["com.android.wm.shell.flicker.splitscreen.CopyContentInSplit"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsSplitScreen-DismissSplitScreenByDivider", + base: "WMShellFlickerTestsSplitScreen", + include_filters: ["com.android.wm.shell.flicker.splitscreen.DismissSplitScreenByDivider"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsSplitScreen-DismissSplitScreenByGoHome", + base: "WMShellFlickerTestsSplitScreen", + include_filters: ["com.android.wm.shell.flicker.splitscreen.DismissSplitScreenByGoHome"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsSplitScreen-DragDividerToResize", + base: "WMShellFlickerTestsSplitScreen", + include_filters: ["com.android.wm.shell.flicker.splitscreen.DragDividerToResize"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsSplitScreen-EnterSplitScreenByDragFromAllApps", + base: "WMShellFlickerTestsSplitScreen", + include_filters: ["com.android.wm.shell.flicker.splitscreen.EnterSplitScreenByDragFromAllApps"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsSplitScreen-EnterSplitScreenByDragFromNotification", + base: "WMShellFlickerTestsSplitScreen", + include_filters: ["com.android.wm.shell.flicker.splitscreen.EnterSplitScreenByDragFromNotification"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsSplitScreen-EnterSplitScreenByDragFromShortcut", + base: "WMShellFlickerTestsSplitScreen", + include_filters: ["com.android.wm.shell.flicker.splitscreen.EnterSplitScreenByDragFromShortcut"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsSplitScreen-EnterSplitScreenByDragFromTaskbar", + base: "WMShellFlickerTestsSplitScreen", + include_filters: ["com.android.wm.shell.flicker.splitscreen.EnterSplitScreenByDragFromTaskbar"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsSplitScreen-EnterSplitScreenFromOverview", + base: "WMShellFlickerTestsSplitScreen", + include_filters: ["com.android.wm.shell.flicker.splitscreen.EnterSplitScreenFromOverview"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsSplitScreen-MultipleShowImeRequestsInSplitScreen", + base: "WMShellFlickerTestsSplitScreen", + include_filters: ["com.android.wm.shell.flicker.splitscreen.MultipleShowImeRequestsInSplitScreen"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsSplitScreen-SwitchAppByDoubleTapDivider", + base: "WMShellFlickerTestsSplitScreen", + include_filters: ["com.android.wm.shell.flicker.splitscreen.SwitchAppByDoubleTapDivider"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsSplitScreen-SwitchBackToSplitFromAnotherApp", + base: "WMShellFlickerTestsSplitScreen", + include_filters: ["com.android.wm.shell.flicker.splitscreen.SwitchBackToSplitFromAnotherApp"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsSplitScreen-SwitchBackToSplitFromHome", + base: "WMShellFlickerTestsSplitScreen", + include_filters: ["com.android.wm.shell.flicker.splitscreen.SwitchBackToSplitFromHome"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsSplitScreen-SwitchBackToSplitFromRecent", + base: "WMShellFlickerTestsSplitScreen", + include_filters: ["com.android.wm.shell.flicker.splitscreen.SwitchBackToSplitFromRecent"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsSplitScreen-SwitchBetweenSplitPairs", + base: "WMShellFlickerTestsSplitScreen", + include_filters: ["com.android.wm.shell.flicker.splitscreen.SwitchBetweenSplitPairs"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsSplitScreen-SwitchBetweenSplitPairsNoPip", + base: "WMShellFlickerTestsSplitScreen", + include_filters: ["com.android.wm.shell.flicker.splitscreen.SwitchBetweenSplitPairsNoPip"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsSplitScreen-UnlockKeyguardToSplitScreen", + base: "WMShellFlickerTestsSplitScreen", + include_filters: ["com.android.wm.shell.flicker.splitscreen.UnlockKeyguardToSplitScreen"], + test_suites: ["device-tests"], +} + +// End breakdowns for FlickerTestsRotation module +//////////////////////////////////////////////////////////////////////////////// diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidManifest.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidManifest.xml index 9ff2161daa51..9ff2161daa51 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidManifest.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidManifest.xml diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml index 85715db3d952..85715db3d952 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/OWNERS b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/OWNERS index 3ab6a1ee061d..3ab6a1ee061d 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/OWNERS +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/OWNERS diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/res/xml/network_security_config.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/res/xml/network_security_config.xml new file mode 100644 index 000000000000..4bd9ca049f55 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/res/xml/network_security_config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<network-security-config> + <domain-config cleartextTrafficPermitted="true"> + <domain includeSubdomains="true">localhost</domain> + </domain-config> +</network-security-config> diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt index 7f48499b0558..7f48499b0558 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt index dd45f654d3bc..dd45f654d3bc 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt index 6d396ea6e9d4..6d396ea6e9d4 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt index 2ed916e56c67..2ed916e56c67 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt index 1a455311b3b6..1a455311b3b6 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt index 0cb1e4006c0d..0cb1e4006c0d 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt index ff406b75b235..ff406b75b235 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt index 2b817988a589..2b817988a589 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt index 186af54fb57b..186af54fb57b 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt index dad5db94d062..a9dba4a3178b 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt @@ -17,11 +17,13 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.Presubmit +import android.tools.NavBar import android.tools.Rotation +import android.tools.ScenarioBuilder import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest -import android.tools.flicker.legacy.LegacyFlickerTestFactory +import android.tools.traces.SERVICE_TRACE_CONFIG import android.tools.traces.component.ComponentNameMatcher import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.splitscreen.benchmark.MultipleShowImeRequestsInSplitScreenBenchmark @@ -35,7 +37,7 @@ import org.junit.runners.Parameterized /** * Test quick switch between two split pairs. * - * To run this test: `atest WMShellFlickerTestsSplitScreenGroup2:MultipleShowImeRequestsInSplitScreen` + * To run this test: `atest WMShellFlickerTestsSplitScreenGroupOther:MultipleShowImeRequestsInSplitScreen` */ @RequiresDevice @RunWith(Parameterized::class) @@ -58,10 +60,22 @@ class MultipleShowImeRequestsInSplitScreen(override val flicker: LegacyFlickerTe } companion object { + private fun createFlickerTest( + navBarMode: NavBar + ) = LegacyFlickerTest(ScenarioBuilder() + .withStartRotation(Rotation.ROTATION_0) + .withEndRotation(Rotation.ROTATION_0) + .withNavBarMode(navBarMode), resultReaderProvider = { scenario -> + android.tools.flicker.datastore.CachedResultReader( + scenario, SERVICE_TRACE_CONFIG + ) + }) + @Parameterized.Parameters(name = "{0}") @JvmStatic - fun getParams() = LegacyFlickerTestFactory.nonRotationTests( - supportedRotations = listOf(Rotation.ROTATION_0) + fun getParams() = listOf( + createFlickerTest(NavBar.MODE_GESTURAL), + createFlickerTest(NavBar.MODE_3BUTTON) ) } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt index 9dde49011ed0..9dde49011ed0 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt index 5222b08240c6..5222b08240c6 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt index a8a8ae88a9e7..a8a8ae88a9e7 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt index 836f664ca544..836f664ca544 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt index 3c4a1caecb8d..3c4a1caecb8d 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairsNoPip.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairsNoPip.kt index a72b3d15eb9e..a72b3d15eb9e 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairsNoPip.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairsNoPip.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt index d34998815fca..3018b56b13b2 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.splitscreen -import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.NavBar import android.tools.flicker.junit.FlickerParametersRunnerFactory diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt index 7e8e50843b90..7e8e50843b90 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt index c99fcc4129d5..c99fcc4129d5 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt index ef3a87955bd6..ef3a87955bd6 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt index 18550d7f0467..18550d7f0467 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt index d16c5d77410c..d16c5d77410c 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt index f8be6be08782..f8be6be08782 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt index a99ef64e7bf5..a99ef64e7bf5 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt index f58400966531..f58400966531 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt index 7084f6aec1fb..7084f6aec1fb 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt index 249253185607..249253185607 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt index 51074f634e30..51074f634e30 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt index 6a6aa1abc9f3..6a6aa1abc9f3 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt index 3a2316f7a10c..3a2316f7a10c 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt index ded0b0729998..ded0b0729998 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt index 7b1397baa7a3..7b1397baa7a3 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt index 457288f445df..457288f445df 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt index 7493538fa2ba..7493538fa2ba 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/trace_config/trace_config.textproto index 67316d2d7c0f..67316d2d7c0f 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/trace_config/trace_config.textproto +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/trace_config/trace_config.textproto diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/Android.bp b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/Android.bp new file mode 100644 index 000000000000..dd0018aae058 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/Android.bp @@ -0,0 +1,41 @@ +// +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "WMShellFlickerServiceTests", + defaults: ["WMShellFlickerTestsDefault"], + manifest: "AndroidManifest.xml", + test_config_template: "AndroidTestTemplate.xml", + srcs: ["src/**/*.kt"], + static_libs: [ + "WMShellFlickerTestsBase", + "WMShellScenariosSplitScreen", + "WMShellTestUtils", + ], + data: [ + ":FlickerTestApp", + "trace_config/*", + ], +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/AndroidManifest.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidManifest.xml index d54b6941d975..662e7f346cb3 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/AndroidManifest.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidManifest.xml @@ -16,7 +16,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" - package="com.android.wm.shell.flicker.service"> + package="com.android.wm.shell"> <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/> <!-- Read and write traces from external storage --> @@ -71,7 +71,7 @@ </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" - android:targetPackage="com.android.wm.shell.flicker.service" + android:targetPackage="com.android.wm.shell" android:label="WindowManager Flicker Service Tests"> </instrumentation> </manifest> diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml new file mode 100644 index 000000000000..6c903a2e8c42 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<configuration description="WMShell Platinum Tests {MODULE}"> + <option name="test-tag" value="FlickerTests"/> + <!-- Needed for storing the perfetto trace files in the sdcard/test_results--> + <option name="isolated-storage" value="false"/> + + <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <!-- disable DeprecatedTargetSdk warning --> + <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> + <!-- keeps the screen on during tests --> + <option name="screen-always-on" value="on"/> + <!-- prevents the phone from restarting --> + <option name="force-skip-system-props" value="true"/> + <!-- set WM tracing verbose level to all --> + <option name="run-command" value="cmd window tracing level all"/> + <!-- set WM tracing to frame (avoid incomplete states) --> + <option name="run-command" value="cmd window tracing frame"/> + <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests --> + <option name="run-command" value="pm disable com.google.android.internal.betterbug"/> + <!-- ensure lock screen mode is swipe --> + <option name="run-command" value="locksettings set-disabled false"/> + <!-- restart launcher to activate TAPL --> + <option name="run-command" + value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher"/> + <!-- Increase trace size: 20mb for WM and 80mb for SF --> + <option name="run-command" value="cmd window tracing size 20480"/> + <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920"/> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="test-user-token" value="%TEST_USER%"/> + <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> + <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> + <option name="run-command" value="settings put system show_touches 1"/> + <option name="run-command" value="settings put system pointer_location 1"/> + <option name="teardown-command" + value="settings delete secure show_ime_with_hard_keyboard"/> + <option name="teardown-command" value="settings delete system show_touches"/> + <option name="teardown-command" value="settings delete system pointer_location"/> + <option name="teardown-command" + value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true"/> + <option name="test-file-name" value="{MODULE}.apk"/> + <option name="test-file-name" value="FlickerTestApp.apk"/> + </target_preparer> + <!-- Needed for pushing the trace config file --> + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="{PACKAGE}"/> + <option name="shell-timeout" value="6600s"/> + <option name="test-timeout" value="6000s"/> + <option name="hidden-api-checks" value="false"/> + <option name="device-listeners" value="android.tools.collectors.DefaultUITraceListener"/> + <!-- DefaultUITraceListener args --> + <option name="instrumentation-arg" key="per_run" value="true"/> + <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/> + </test> + <!-- Needed for pulling the collected trace config on to the host --> + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="pull-pattern-keys" value="perfetto_file_path"/> + <option name="directory-keys" + value="/data/user/0/com.android.wm.shell/files"/> + <option name="collect-on-run-ended-only" value="true"/> + <option name="clean-up" value="true"/> + </metrics_collector> +</configuration> diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/res/xml/network_security_config.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/res/xml/network_security_config.xml new file mode 100644 index 000000000000..4bd9ca049f55 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/res/xml/network_security_config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<network-security-config> + <domain-config cleartextTrafficPermitted="true"> + <domain includeSubdomains="true">localhost</domain> + </domain-config> +</network-security-config> diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/CopyContentInSplitGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/CopyContentInSplitGesturalNavLandscape.kt index 1684a26ac3d2..3cb9cf24d522 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/CopyContentInSplitGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/CopyContentInSplitGesturalNavLandscape.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.CopyContentInSplit +import com.android.wm.shell.scenarios.CopyContentInSplit import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/CopyContentInSplitGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/CopyContentInSplitGesturalNavPortrait.kt index 3b5fad60d8ee..b27a8aedb5c1 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/CopyContentInSplitGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/CopyContentInSplitGesturalNavPortrait.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.CopyContentInSplit +import com.android.wm.shell.scenarios.CopyContentInSplit import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByDividerGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/DismissSplitScreenByDividerGesturalNavLandscape.kt index 2b8a90305d90..9388114889c0 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByDividerGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/DismissSplitScreenByDividerGesturalNavLandscape.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByDivider +import com.android.wm.shell.scenarios.DismissSplitScreenByDivider import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByDividerGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/DismissSplitScreenByDividerGesturalNavPortrait.kt index b284fe1caad5..30ef4927bfa0 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByDividerGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/DismissSplitScreenByDividerGesturalNavPortrait.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByDivider +import com.android.wm.shell.scenarios.DismissSplitScreenByDivider import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByGoHomeGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/DismissSplitScreenByGoHomeGesturalNavLandscape.kt index a400ee44caa5..059f967fbb51 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByGoHomeGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/DismissSplitScreenByGoHomeGesturalNavLandscape.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByGoHome +import com.android.wm.shell.scenarios.DismissSplitScreenByGoHome import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByGoHomeGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/DismissSplitScreenByGoHomeGesturalNavPortrait.kt index 7f5ee4c2cdda..0c6d546cf8e5 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DismissSplitScreenByGoHomeGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/DismissSplitScreenByGoHomeGesturalNavPortrait.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByGoHome +import com.android.wm.shell.scenarios.DismissSplitScreenByGoHome import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DragDividerToResizeGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/DragDividerToResizeGesturalNavLandscape.kt index 1b075c498bc0..14fb72bc349a 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DragDividerToResizeGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/DragDividerToResizeGesturalNavLandscape.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.DragDividerToResize +import com.android.wm.shell.scenarios.DragDividerToResize import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DragDividerToResizeGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/DragDividerToResizeGesturalNavPortrait.kt index 6ca373714f8a..9be61a59f101 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/DragDividerToResizeGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/DragDividerToResizeGesturalNavPortrait.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.DragDividerToResize +import com.android.wm.shell.scenarios.DragDividerToResize import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromAllAppsGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenByDragFromAllAppsGesturalNavLandscape.kt index f7d231f02935..c12d1990d3ed 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromAllAppsGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenByDragFromAllAppsGesturalNavLandscape.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromAllApps +import com.android.wm.shell.scenarios.EnterSplitScreenByDragFromAllApps import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromAllAppsGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenByDragFromAllAppsGesturalNavPortrait.kt index ab819fad292a..11cd38ad72a4 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromAllAppsGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenByDragFromAllAppsGesturalNavPortrait.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromAllApps +import com.android.wm.shell.scenarios.EnterSplitScreenByDragFromAllApps import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromNotificationGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenByDragFromNotificationGesturalNavLandscape.kt index a6b732c47ea2..66d4bfa977c4 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromNotificationGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenByDragFromNotificationGesturalNavLandscape.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromNotification +import com.android.wm.shell.scenarios.EnterSplitScreenByDragFromNotification import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromNotificationGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenByDragFromNotificationGesturalNavPortrait.kt index 07e5f4b0b472..f3a11eb96101 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromNotificationGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenByDragFromNotificationGesturalNavPortrait.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromNotification +import com.android.wm.shell.scenarios.EnterSplitScreenByDragFromNotification import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromShortcutGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenByDragFromShortcutGesturalNavLandscape.kt index 272569456d7b..327ecc34a01e 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromShortcutGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenByDragFromShortcutGesturalNavLandscape.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromShortcut +import com.android.wm.shell.scenarios.EnterSplitScreenByDragFromShortcut import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromShortcutGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenByDragFromShortcutGesturalNavPortrait.kt index 58cc4d70fde4..dd5a3950537c 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromShortcutGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenByDragFromShortcutGesturalNavPortrait.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromShortcut +import com.android.wm.shell.scenarios.EnterSplitScreenByDragFromShortcut import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromTaskbarGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenByDragFromTaskbarGesturalNavLandscape.kt index 85897a136e33..8e7cf309c911 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromTaskbarGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenByDragFromTaskbarGesturalNavLandscape.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromTaskbar +import com.android.wm.shell.scenarios.EnterSplitScreenByDragFromTaskbar import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromTaskbarGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenByDragFromTaskbarGesturalNavPortrait.kt index 891b6df89b45..0324dac44b3a 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenByDragFromTaskbarGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenByDragFromTaskbarGesturalNavPortrait.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromTaskbar +import com.android.wm.shell.scenarios.EnterSplitScreenByDragFromTaskbar import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenFromOverviewGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenFromOverviewGesturalNavLandscape.kt index 798365218b04..2fa141eb9a91 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenFromOverviewGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenFromOverviewGesturalNavLandscape.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenFromOverview +import com.android.wm.shell.scenarios.EnterSplitScreenFromOverview import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenFromOverviewGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenFromOverviewGesturalNavPortrait.kt index 1bdea66fc596..01769138ddff 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/EnterSplitScreenFromOverviewGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/EnterSplitScreenFromOverviewGesturalNavPortrait.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenFromOverview +import com.android.wm.shell.scenarios.EnterSplitScreenFromOverview import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchAppByDoubleTapDividerGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchAppByDoubleTapDividerGesturalNavLandscape.kt index bab0c0aa1e6a..1db28dcc4f09 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchAppByDoubleTapDividerGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchAppByDoubleTapDividerGesturalNavLandscape.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchAppByDoubleTapDivider +import com.android.wm.shell.scenarios.SwitchAppByDoubleTapDivider import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchAppByDoubleTapDividerGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchAppByDoubleTapDividerGesturalNavPortrait.kt index 17a59ab8a173..c69167bcf67f 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchAppByDoubleTapDividerGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchAppByDoubleTapDividerGesturalNavPortrait.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchAppByDoubleTapDivider +import com.android.wm.shell.scenarios.SwitchAppByDoubleTapDivider import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromAnotherAppGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchBackToSplitFromAnotherAppGesturalNavLandscape.kt index 2c36d647b719..602283a136b3 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromAnotherAppGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchBackToSplitFromAnotherAppGesturalNavLandscape.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromAnotherApp +import com.android.wm.shell.scenarios.SwitchBackToSplitFromAnotherApp import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromAnotherAppGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchBackToSplitFromAnotherAppGesturalNavPortrait.kt index 6e91d047e64b..7cc14e091540 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromAnotherAppGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchBackToSplitFromAnotherAppGesturalNavPortrait.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromAnotherApp +import com.android.wm.shell.scenarios.SwitchBackToSplitFromAnotherApp import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromHomeGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchBackToSplitFromHomeGesturalNavLandscape.kt index a921b4663d09..daf6547673af 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromHomeGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchBackToSplitFromHomeGesturalNavLandscape.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromHome +import com.android.wm.shell.scenarios.SwitchBackToSplitFromHome import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromHomeGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchBackToSplitFromHomeGesturalNavPortrait.kt index 05f8912f6f47..b0f5e6564b97 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromHomeGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchBackToSplitFromHomeGesturalNavPortrait.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromHome +import com.android.wm.shell.scenarios.SwitchBackToSplitFromHome import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromRecentGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchBackToSplitFromRecentGesturalNavLandscape.kt index 1ae1f53b9bc1..88fa783a679d 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromRecentGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchBackToSplitFromRecentGesturalNavLandscape.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromRecent +import com.android.wm.shell.scenarios.SwitchBackToSplitFromRecent import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromRecentGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchBackToSplitFromRecentGesturalNavPortrait.kt index e14ca550c84d..aa36f44fd499 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBackToSplitFromRecentGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchBackToSplitFromRecentGesturalNavPortrait.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromRecent +import com.android.wm.shell.scenarios.SwitchBackToSplitFromRecent import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBetweenSplitPairsGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchBetweenSplitPairsGesturalNavLandscape.kt index ce0c4c456587..292f413e0037 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBetweenSplitPairsGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchBetweenSplitPairsGesturalNavLandscape.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBetweenSplitPairs +import com.android.wm.shell.scenarios.SwitchBetweenSplitPairs import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBetweenSplitPairsGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchBetweenSplitPairsGesturalNavPortrait.kt index 5a8d2d51bec4..865958fe82b4 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/SwitchBetweenSplitPairsGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/SwitchBetweenSplitPairsGesturalNavPortrait.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.Rotation import android.tools.flicker.FlickerConfig @@ -23,7 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBetweenSplitPairs +import com.android.wm.shell.scenarios.SwitchBetweenSplitPairs import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/UnlockKeyguardToSplitScreenGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/UnlockKeyguardToSplitScreenGesturalNavLandscape.kt index d44261549544..6c36e8476cc3 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/UnlockKeyguardToSplitScreenGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/UnlockKeyguardToSplitScreenGesturalNavLandscape.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.flicker.FlickerConfig import android.tools.flicker.annotation.ExpectedScenarios @@ -22,7 +22,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.UnlockKeyguardToSplitScreen +import com.android.wm.shell.scenarios.UnlockKeyguardToSplitScreen import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/UnlockKeyguardToSplitScreenGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/UnlockKeyguardToSplitScreenGesturalNavPortrait.kt index ddc8a0697beb..61ccd36dd106 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/flicker/UnlockKeyguardToSplitScreenGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/src/com/android/server/wm/shell/flicker/UnlockKeyguardToSplitScreenGesturalNavPortrait.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.flicker +package com.android.wm.shell.flicker import android.tools.flicker.FlickerConfig import android.tools.flicker.annotation.ExpectedScenarios @@ -22,7 +22,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import com.android.wm.shell.flicker.service.splitscreen.scenarios.UnlockKeyguardToSplitScreen +import com.android.wm.shell.scenarios.UnlockKeyguardToSplitScreen import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/service/Android.bp b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/Android.bp index a5bc26152d16..90210b1262c7 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/Android.bp +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/Android.bp @@ -26,9 +26,7 @@ package { filegroup { name: "WMShellFlickerServicePlatinumTests-src", srcs: [ - "src/**/platinum/*.kt", - "src/**/scenarios/*.kt", - "src/**/common/*.kt", + "src/**/*.kt", ], } @@ -43,33 +41,42 @@ java_library { ], static_libs: [ "wm-shell-flicker-utils", + "WMShellScenariosSplitScreen", ], } android_test { - name: "WMShellFlickerServiceTests", - defaults: ["WMShellFlickerTestsDefault"], - manifest: "AndroidManifest.xml", - package_name: "com.android.wm.shell.flicker.service", - instrumentation_target_package: "com.android.wm.shell.flicker.service", - test_config_template: "AndroidTestTemplate.xml", - srcs: ["src/**/*.kt"], - static_libs: ["WMShellFlickerTestsBase"], - data: ["trace_config/*"], -} - -android_test { name: "WMShellFlickerServicePlatinumTests", - defaults: ["WMShellFlickerTestsDefault"], + platform_apis: true, + certificate: "platform", + optimize: { + enabled: false, + }, manifest: "AndroidManifest.xml", - package_name: "com.android.wm.shell.flicker.service", - instrumentation_target_package: "com.android.wm.shell.flicker.service", test_config_template: "AndroidTestTemplate.xml", test_suites: [ "device-tests", "device-platinum-tests", ], srcs: [":WMShellFlickerServicePlatinumTests-src"], - static_libs: ["WMShellFlickerTestsBase"], - data: ["trace_config/*"], + static_libs: [ + "WMShellFlickerTestsBase", + "WMShellScenariosSplitScreen", + "WMShellTestUtils", + "ui-trace-collector", + "collector-device-lib", + "wm-shell-flicker-utils", + "androidx.test.ext.junit", + "flickertestapplib", + "flickerlib-helpers", + "flickerlib-trace_processor_shell", + "platform-test-annotations", + "wm-flicker-common-app-helpers", + "launcher-helper-lib", + "launcher-aosp-tapl", + ], + data: [ + ":FlickerTestApp", + "trace_config/*", + ], } diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidManifest.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidManifest.xml new file mode 100644 index 000000000000..662e7f346cb3 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidManifest.xml @@ -0,0 +1,77 @@ +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.android.wm.shell"> + + <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/> + <!-- Read and write traces from external storage --> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + <!-- Allow the test to write directly to /sdcard/ --> + <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> + <!-- Write secure settings --> + <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> + <!-- Capture screen contents --> + <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" /> + <!-- Enable / Disable tracing !--> + <uses-permission android:name="android.permission.DUMP" /> + <!-- Run layers trace --> + <uses-permission android:name="android.permission.HARDWARE_TEST"/> + <!-- Capture screen recording --> + <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT"/> + <!-- Workaround grant runtime permission exception from b/152733071 --> + <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/> + <uses-permission android:name="android.permission.READ_LOGS"/> + <!-- Force-stop test apps --> + <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES"/> + <!-- Control test app's media session --> + <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/> + <!-- ATM.removeRootTasksWithActivityTypes() --> + <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" /> + <!-- Enable bubble notification--> + <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" /> + <!-- Allow the test to connect to perfetto trace processor --> + <uses-permission android:name="android.permission.INTERNET"/> + + <!-- Allow the test to write directly to /sdcard/ and connect to trace processor --> + <application android:requestLegacyExternalStorage="true" + android:networkSecurityConfig="@xml/network_security_config" + android:largeHeap="true"> + <uses-library android:name="android.test.runner"/> + + <service android:name=".NotificationListener" + android:exported="true" + android:label="WMShellTestsNotificationListenerService" + android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"> + <intent-filter> + <action android:name="android.service.notification.NotificationListenerService" /> + </intent-filter> + </service> + + <!-- (b/197936012) Remove startup provider due to test timeout issue --> + <provider + android:name="androidx.startup.InitializationProvider" + android:authorities="${applicationId}.androidx-startup" + tools:node="remove" /> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.wm.shell" + android:label="WindowManager Flicker Service Tests"> + </instrumentation> +</manifest> diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml new file mode 100644 index 000000000000..6c903a2e8c42 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<configuration description="WMShell Platinum Tests {MODULE}"> + <option name="test-tag" value="FlickerTests"/> + <!-- Needed for storing the perfetto trace files in the sdcard/test_results--> + <option name="isolated-storage" value="false"/> + + <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <!-- disable DeprecatedTargetSdk warning --> + <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> + <!-- keeps the screen on during tests --> + <option name="screen-always-on" value="on"/> + <!-- prevents the phone from restarting --> + <option name="force-skip-system-props" value="true"/> + <!-- set WM tracing verbose level to all --> + <option name="run-command" value="cmd window tracing level all"/> + <!-- set WM tracing to frame (avoid incomplete states) --> + <option name="run-command" value="cmd window tracing frame"/> + <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests --> + <option name="run-command" value="pm disable com.google.android.internal.betterbug"/> + <!-- ensure lock screen mode is swipe --> + <option name="run-command" value="locksettings set-disabled false"/> + <!-- restart launcher to activate TAPL --> + <option name="run-command" + value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher"/> + <!-- Increase trace size: 20mb for WM and 80mb for SF --> + <option name="run-command" value="cmd window tracing size 20480"/> + <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920"/> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="test-user-token" value="%TEST_USER%"/> + <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> + <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> + <option name="run-command" value="settings put system show_touches 1"/> + <option name="run-command" value="settings put system pointer_location 1"/> + <option name="teardown-command" + value="settings delete secure show_ime_with_hard_keyboard"/> + <option name="teardown-command" value="settings delete system show_touches"/> + <option name="teardown-command" value="settings delete system pointer_location"/> + <option name="teardown-command" + value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true"/> + <option name="test-file-name" value="{MODULE}.apk"/> + <option name="test-file-name" value="FlickerTestApp.apk"/> + </target_preparer> + <!-- Needed for pushing the trace config file --> + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="{PACKAGE}"/> + <option name="shell-timeout" value="6600s"/> + <option name="test-timeout" value="6000s"/> + <option name="hidden-api-checks" value="false"/> + <option name="device-listeners" value="android.tools.collectors.DefaultUITraceListener"/> + <!-- DefaultUITraceListener args --> + <option name="instrumentation-arg" key="per_run" value="true"/> + <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/> + </test> + <!-- Needed for pulling the collected trace config on to the host --> + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="pull-pattern-keys" value="perfetto_file_path"/> + <option name="directory-keys" + value="/data/user/0/com.android.wm.shell/files"/> + <option name="collect-on-run-ended-only" value="true"/> + <option name="clean-up" value="true"/> + </metrics_collector> +</configuration> diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/res/xml/network_security_config.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/res/xml/network_security_config.xml new file mode 100644 index 000000000000..4bd9ca049f55 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/res/xml/network_security_config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<network-security-config> + <domain-config cleartextTrafficPermitted="true"> + <domain includeSubdomains="true">localhost</domain> + </domain-config> +</network-security-config> diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/CopyContentInSplitGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/CopyContentInSplitGesturalNavLandscape.kt index 64293b288a2e..4c2ca6763fc2 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/CopyContentInSplitGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/CopyContentInSplitGesturalNavLandscape.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.CopyContentInSplit +import com.android.wm.shell.scenarios.CopyContentInSplit import org.junit.Test open class CopyContentInSplitGesturalNavLandscape : CopyContentInSplit(Rotation.ROTATION_90) { diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/CopyContentInSplitGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/CopyContentInSplitGesturalNavPortrait.kt index 517ba2dfc164..0cca31002722 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/CopyContentInSplitGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/CopyContentInSplitGesturalNavPortrait.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.CopyContentInSplit +import com.android.wm.shell.scenarios.CopyContentInSplit import org.junit.Test open class CopyContentInSplitGesturalNavPortrait : CopyContentInSplit(Rotation.ROTATION_0) { diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByDividerGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/DismissSplitScreenByDividerGesturalNavLandscape.kt index 1bafe3b0898c..7aa62cf906e4 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByDividerGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/DismissSplitScreenByDividerGesturalNavLandscape.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByDivider +import com.android.wm.shell.scenarios.DismissSplitScreenByDivider import org.junit.Test open class DismissSplitScreenByDividerGesturalNavLandscape : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByDividerGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/DismissSplitScreenByDividerGesturalNavPortrait.kt index fd0100fd6c21..de11fc6774a2 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByDividerGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/DismissSplitScreenByDividerGesturalNavPortrait.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByDivider +import com.android.wm.shell.scenarios.DismissSplitScreenByDivider import org.junit.Test open class DismissSplitScreenByDividerGesturalNavPortrait : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByGoHomeGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/DismissSplitScreenByGoHomeGesturalNavLandscape.kt index 850b3d8f9867..daa6aac30285 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByGoHomeGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/DismissSplitScreenByGoHomeGesturalNavLandscape.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByGoHome +import com.android.wm.shell.scenarios.DismissSplitScreenByGoHome import org.junit.Test open class DismissSplitScreenByGoHomeGesturalNavLandscape : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByGoHomeGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/DismissSplitScreenByGoHomeGesturalNavPortrait.kt index 0b752bf7f58e..ff57d0057039 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DismissSplitScreenByGoHomeGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/DismissSplitScreenByGoHomeGesturalNavPortrait.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByGoHome +import com.android.wm.shell.scenarios.DismissSplitScreenByGoHome import org.junit.Test open class DismissSplitScreenByGoHomeGesturalNavPortrait : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DragDividerToResizeGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/DragDividerToResizeGesturalNavLandscape.kt index 3c52aa71eb9d..0ac19c8d8452 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DragDividerToResizeGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/DragDividerToResizeGesturalNavLandscape.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.DragDividerToResize +import com.android.wm.shell.scenarios.DragDividerToResize import org.junit.Test open class DragDividerToResizeGesturalNavLandscape : DragDividerToResize(Rotation.ROTATION_90) { diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DragDividerToResizeGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/DragDividerToResizeGesturalNavPortrait.kt index c2e21b89a480..5713602e7136 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/DragDividerToResizeGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/DragDividerToResizeGesturalNavPortrait.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.DragDividerToResize +import com.android.wm.shell.scenarios.DragDividerToResize import org.junit.Test open class DragDividerToResizeGesturalNavPortrait : DragDividerToResize(Rotation.ROTATION_0) { diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromAllAppsGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenByDragFromAllAppsGesturalNavLandscape.kt index bf85ab44df5e..d7333f1a4eda 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromAllAppsGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenByDragFromAllAppsGesturalNavLandscape.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromAllApps +import com.android.wm.shell.scenarios.EnterSplitScreenByDragFromAllApps import org.junit.Test open class EnterSplitScreenByDragFromAllAppsGesturalNavLandscape : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromAllAppsGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenByDragFromAllAppsGesturalNavPortrait.kt index 0ac4ca20e303..e29a140e4bdf 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromAllAppsGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenByDragFromAllAppsGesturalNavPortrait.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromAllApps +import com.android.wm.shell.scenarios.EnterSplitScreenByDragFromAllApps import org.junit.Test open class EnterSplitScreenByDragFromAllAppsGesturalNavPortrait : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromNotificationGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenByDragFromNotificationGesturalNavLandscape.kt index 80bd088a192f..9ccccb1da273 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromNotificationGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenByDragFromNotificationGesturalNavLandscape.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromNotification +import com.android.wm.shell.scenarios.EnterSplitScreenByDragFromNotification import org.junit.Test open class EnterSplitScreenByDragFromNotificationGesturalNavLandscape : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromNotificationGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenByDragFromNotificationGesturalNavPortrait.kt index 0dffb4af8d41..87a4d0847b96 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromNotificationGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenByDragFromNotificationGesturalNavPortrait.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromNotification +import com.android.wm.shell.scenarios.EnterSplitScreenByDragFromNotification import org.junit.Test open class EnterSplitScreenByDragFromNotificationGesturalNavPortrait : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromShortcutGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenByDragFromShortcutGesturalNavLandscape.kt index b721f2fe294a..559652c1e8a5 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromShortcutGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenByDragFromShortcutGesturalNavLandscape.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromShortcut +import com.android.wm.shell.scenarios.EnterSplitScreenByDragFromShortcut import org.junit.Test open class EnterSplitScreenByDragFromShortcutGesturalNavLandscape : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromShortcutGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenByDragFromShortcutGesturalNavPortrait.kt index 22cbc77d024b..bcb8e0c698af 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromShortcutGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenByDragFromShortcutGesturalNavPortrait.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromShortcut +import com.android.wm.shell.scenarios.EnterSplitScreenByDragFromShortcut import org.junit.Test open class EnterSplitScreenByDragFromShortcutGesturalNavPortrait : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromTaskbarGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenByDragFromTaskbarGesturalNavLandscape.kt index ac0f9e25285d..39e0fede7fae 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromTaskbarGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenByDragFromTaskbarGesturalNavLandscape.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromTaskbar +import com.android.wm.shell.scenarios.EnterSplitScreenByDragFromTaskbar import org.junit.Test open class EnterSplitScreenByDragFromTaskbarGesturalNavLandscape : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromTaskbarGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenByDragFromTaskbarGesturalNavPortrait.kt index f7a229d5ff16..643162926f79 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenByDragFromTaskbarGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenByDragFromTaskbarGesturalNavPortrait.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromTaskbar +import com.android.wm.shell.scenarios.EnterSplitScreenByDragFromTaskbar import org.junit.Test open class EnterSplitScreenByDragFromTaskbarGesturalNavPortrait : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenFromOverviewGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenFromOverviewGesturalNavLandscape.kt index 6dbbcb0fcfb5..2093424ea1de 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenFromOverviewGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenFromOverviewGesturalNavLandscape.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenFromOverview +import com.android.wm.shell.scenarios.EnterSplitScreenFromOverview import org.junit.Test open class EnterSplitScreenFromOverviewGesturalNavLandscape : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenFromOverviewGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenFromOverviewGesturalNavPortrait.kt index bd69ea98a67b..f89259d496b2 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/EnterSplitScreenFromOverviewGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/EnterSplitScreenFromOverviewGesturalNavPortrait.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenFromOverview +import com.android.wm.shell.scenarios.EnterSplitScreenFromOverview import org.junit.Test open class EnterSplitScreenFromOverviewGesturalNavPortrait : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchAppByDoubleTapDividerGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchAppByDoubleTapDividerGesturalNavLandscape.kt index 404b96fafd24..e5aff0c0800f 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchAppByDoubleTapDividerGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchAppByDoubleTapDividerGesturalNavLandscape.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchAppByDoubleTapDivider +import com.android.wm.shell.scenarios.SwitchAppByDoubleTapDivider import org.junit.Test open class SwitchAppByDoubleTapDividerGesturalNavLandscape : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchAppByDoubleTapDividerGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchAppByDoubleTapDividerGesturalNavPortrait.kt index a79687ddf68f..defade909913 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchAppByDoubleTapDividerGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchAppByDoubleTapDividerGesturalNavPortrait.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchAppByDoubleTapDivider +import com.android.wm.shell.scenarios.SwitchAppByDoubleTapDivider import org.junit.Test open class SwitchAppByDoubleTapDividerGesturalNavPortrait : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromAnotherAppGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchBackToSplitFromAnotherAppGesturalNavLandscape.kt index b52eb4cd6533..e28deca37b24 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromAnotherAppGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchBackToSplitFromAnotherAppGesturalNavLandscape.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromAnotherApp +import com.android.wm.shell.scenarios.SwitchBackToSplitFromAnotherApp import org.junit.Test open class SwitchBackToSplitFromAnotherAppGesturalNavLandscape : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromAnotherAppGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchBackToSplitFromAnotherAppGesturalNavPortrait.kt index d79620c73132..99fb06c20a0b 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromAnotherAppGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchBackToSplitFromAnotherAppGesturalNavPortrait.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromAnotherApp +import com.android.wm.shell.scenarios.SwitchBackToSplitFromAnotherApp import org.junit.Test open class SwitchBackToSplitFromAnotherAppGesturalNavPortrait : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromHomeGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchBackToSplitFromHomeGesturalNavLandscape.kt index d27bfa1a22c9..7045e660b8b8 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromHomeGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchBackToSplitFromHomeGesturalNavLandscape.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromHome +import com.android.wm.shell.scenarios.SwitchBackToSplitFromHome import org.junit.Test open class SwitchBackToSplitFromHomeGesturalNavLandscape : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromHomeGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchBackToSplitFromHomeGesturalNavPortrait.kt index 3c7d4d4806cf..b2da052c6209 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromHomeGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchBackToSplitFromHomeGesturalNavPortrait.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromHome +import com.android.wm.shell.scenarios.SwitchBackToSplitFromHome import org.junit.Test open class SwitchBackToSplitFromHomeGesturalNavPortrait : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromRecentGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchBackToSplitFromRecentGesturalNavLandscape.kt index 26a2034f16d9..04d7f62ecf53 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromRecentGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchBackToSplitFromRecentGesturalNavLandscape.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromRecent +import com.android.wm.shell.scenarios.SwitchBackToSplitFromRecent import org.junit.Test open class SwitchBackToSplitFromRecentGesturalNavLandscape : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromRecentGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchBackToSplitFromRecentGesturalNavPortrait.kt index 5154b35ed0e6..bc36fb748226 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBackToSplitFromRecentGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchBackToSplitFromRecentGesturalNavPortrait.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromRecent +import com.android.wm.shell.scenarios.SwitchBackToSplitFromRecent import org.junit.Test open class SwitchBackToSplitFromRecentGesturalNavPortrait : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBetweenSplitPairsGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchBetweenSplitPairsGesturalNavLandscape.kt index 86451c524ee6..ceda4da6a94f 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBetweenSplitPairsGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchBetweenSplitPairsGesturalNavLandscape.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBetweenSplitPairs +import com.android.wm.shell.scenarios.SwitchBetweenSplitPairs import org.junit.Test open class SwitchBetweenSplitPairsGesturalNavLandscape : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBetweenSplitPairsGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchBetweenSplitPairsGesturalNavPortrait.kt index baf72b40d6d4..365c5cc34d11 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/SwitchBetweenSplitPairsGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/SwitchBetweenSplitPairsGesturalNavPortrait.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.Rotation -import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBetweenSplitPairs +import com.android.wm.shell.scenarios.SwitchBetweenSplitPairs import org.junit.Test open class SwitchBetweenSplitPairsGesturalNavPortrait : diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/UnlockKeyguardToSplitScreenGesturalNavLandscape.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/UnlockKeyguardToSplitScreenGesturalNavLandscape.kt index 9caab9b5182a..a8662979407e 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/UnlockKeyguardToSplitScreenGesturalNavLandscape.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/UnlockKeyguardToSplitScreenGesturalNavLandscape.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit -import com.android.wm.shell.flicker.service.splitscreen.scenarios.UnlockKeyguardToSplitScreen +import com.android.wm.shell.scenarios.UnlockKeyguardToSplitScreen import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.BlockJUnit4ClassRunner diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/UnlockKeyguardToSplitScreenGesturalNavPortrait.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/UnlockKeyguardToSplitScreenGesturalNavPortrait.kt index bf484e5cef98..6d59001374e9 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/platinum/UnlockKeyguardToSplitScreenGesturalNavPortrait.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/src/com/android/wm/shell/UnlockKeyguardToSplitScreenGesturalNavPortrait.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.platinum +package com.android.wm.shell import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit -import com.android.wm.shell.flicker.service.splitscreen.scenarios.UnlockKeyguardToSplitScreen +import com.android.wm.shell.scenarios.UnlockKeyguardToSplitScreen import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.BlockJUnit4ClassRunner diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/Android.bp b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/Android.bp new file mode 100644 index 000000000000..60c7de7b3931 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/Android.bp @@ -0,0 +1,47 @@ +// +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library { + name: "WMShellScenariosSplitScreen", + platform_apis: true, + optimize: { + enabled: false, + }, + srcs: ["src/**/*.kt"], + static_libs: [ + "WMShellFlickerTestsBase", + "WMShellTestUtils", + "wm-shell-flicker-utils", + "androidx.test.ext.junit", + "flickertestapplib", + "flickerlib-helpers", + "flickerlib-trace_processor_shell", + "platform-test-annotations", + "wm-flicker-common-app-helpers", + "wm-flicker-common-assertions", + "launcher-helper-lib", + "launcher-aosp-tapl", + ], +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt index 61710742abb4..ba4654260864 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.scenarios +package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar @@ -23,7 +23,7 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation -import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt index c1a8ee714abd..d774a31220da 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByDivider.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.scenarios +package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar @@ -23,7 +23,7 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation -import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt index 600855a8ab38..5aa161911a9a 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByGoHome.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.scenarios +package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar @@ -23,7 +23,7 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation -import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt index c671fbe39ac5..668f3678bb38 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DragDividerToResize.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.scenarios +package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar @@ -23,7 +23,7 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation -import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before @@ -48,6 +48,8 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { fun setup() { tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) + // TODO: b/349075982 - Remove once launcher rotation and checks are stable. + tapl.setExpectedRotationCheckEnabled(false) SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp, rotation) } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/EnterSplitScreenByDragFromAllApps.kt index a189325d52ea..06c7b9bc384d 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/EnterSplitScreenByDragFromAllApps.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.scenarios +package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar @@ -24,7 +24,7 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation -import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Assume diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/EnterSplitScreenByDragFromNotification.kt index bcd0f126daef..96b22bf86742 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/EnterSplitScreenByDragFromNotification.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.scenarios +package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar @@ -25,7 +25,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.helpers.MultiWindowUtils -import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Assume diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/EnterSplitScreenByDragFromShortcut.kt index 3f07be083041..9e05b630d840 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/EnterSplitScreenByDragFromShortcut.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.scenarios +package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar @@ -24,7 +24,7 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation -import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Assume diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/EnterSplitScreenByDragFromTaskbar.kt index 532801357d60..90900557e582 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/EnterSplitScreenByDragFromTaskbar.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.scenarios +package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar @@ -23,7 +23,7 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation -import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Assume diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/EnterSplitScreenFromOverview.kt index be4035d6af7f..d5cc92e5d268 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/EnterSplitScreenFromOverview.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.scenarios +package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar @@ -23,7 +23,7 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation -import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/SwitchAppByDoubleTapDivider.kt index db962e717a3b..26203d4afccd 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/SwitchAppByDoubleTapDivider.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.scenarios +package com.android.wm.shell.scenarios import android.app.Instrumentation import android.graphics.Point @@ -25,7 +25,7 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation -import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before @@ -48,7 +48,10 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { @Before fun setup() { - tapl.workspace.switchToOverview().dismissAllTasks() + val overview = tapl.workspace.switchToOverview() + if (overview.hasTasks()) { + overview.dismissAllTasks() + } tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/SwitchBackToSplitFromAnotherApp.kt index de26982501a3..2ccffa85b5c1 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/SwitchBackToSplitFromAnotherApp.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.scenarios +package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar @@ -23,7 +23,7 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation -import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/SwitchBackToSplitFromHome.kt index 873b0199f0e8..8673c464ad19 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/SwitchBackToSplitFromHome.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.scenarios +package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar @@ -23,7 +23,7 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation -import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/SwitchBackToSplitFromRecent.kt index 15934d0f3944..22adf6c9ee2f 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/SwitchBackToSplitFromRecent.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.scenarios +package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar @@ -23,7 +23,7 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation -import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before @@ -48,6 +48,7 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { fun setup() { tapl.workspace.switchToOverview().dismissAllTasks() + tapl.setExpectedRotationCheckEnabled(false) tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/SwitchBetweenSplitPairs.kt index 79e69ae084f4..4ded148f6113 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/SwitchBetweenSplitPairs.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.scenarios +package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar @@ -23,7 +23,7 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation -import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/UnlockKeyguardToSplitScreen.kt index 0f932d46d3d3..7b062fcc6b5a 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/UnlockKeyguardToSplitScreen.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.splitscreen.scenarios +package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar @@ -23,7 +23,7 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation -import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before diff --git a/libs/WindowManager/Shell/tests/e2e/utils/Android.bp b/libs/WindowManager/Shell/tests/e2e/utils/Android.bp new file mode 100644 index 000000000000..51d9c401dfba --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/utils/Android.bp @@ -0,0 +1,31 @@ +// +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_team: "trendy_team_windowing_tools", + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library { + name: "WMShellTestUtils", + srcs: ["src/**/*.kt"], + static_libs: ["WMShellFlickerTestsBase"], +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/common/Utils.kt b/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/Utils.kt index 4c6c6cce0105..c0fafef96775 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/common/Utils.kt +++ b/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/Utils.kt @@ -14,9 +14,10 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.service.common +package com.android.wm.shell import android.app.Instrumentation +import android.platform.test.rule.EnsureDeviceSettingsRule import android.platform.test.rule.NavigationModeRule import android.platform.test.rule.PressHomeRule import android.platform.test.rule.UnlockScreenRule @@ -49,5 +50,6 @@ object Utils { ) ) .around(PressHomeRule()) + .around(EnsureDeviceSettingsRule()) } } diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp index 4058fa977f1d..58559ac2c3ca 100644 --- a/libs/WindowManager/Shell/tests/flicker/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/Android.bp @@ -68,6 +68,7 @@ java_defaults { "flickerlib-helpers", "flickerlib-trace_processor_shell", "platform-test-annotations", + "platform-test-rules", "wm-flicker-common-app-helpers", "wm-flicker-common-assertions", "launcher-helper-lib", diff --git a/libs/WindowManager/Shell/tests/flicker/OWNERS b/libs/WindowManager/Shell/tests/flicker/OWNERS new file mode 100644 index 000000000000..4db0babdaf99 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/OWNERS @@ -0,0 +1,3 @@ +# Bug component: 1157642 +# includes OWNERS from parent directories +include platform/development:/tools/winscope/OWNERS diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp b/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp index e151ab2c5878..29a9f1050b25 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp @@ -15,6 +15,7 @@ // package { + default_team: "trendy_team_app_compat", // See: http://go/android-license-faq // A large-scale-change added 'default_applicable_licenses' to import // all of the 'license_kinds' from "frameworks_base_license" @@ -23,6 +24,9 @@ package { default_applicable_licenses: ["frameworks_base_license"], } +//////////////////////////////////////////////////////////////////////////////// +// Begin cleanup after gcl merge + filegroup { name: "WMShellFlickerTestsAppCompat-src", srcs: [ @@ -41,3 +45,80 @@ android_test { static_libs: ["WMShellFlickerTestsBase"], data: ["trace_config/*"], } + +//////////////////////////////////////////////////////////////////////////////// +// End cleanup after gcl merge + +android_test { + name: "WMShellFlickerTestsAppCompat", + defaults: ["WMShellFlickerTestsDefault"], + manifest: "AndroidManifest.xml", + package_name: "com.android.wm.shell.flicker", + instrumentation_target_package: "com.android.wm.shell.flicker", + test_config_template: "AndroidTestTemplate.xml", + srcs: ["src/**/*.kt"], + static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], +} + +//////////////////////////////////////////////////////////////////////////////// +// Begin breakdowns for WMShellFlickerTestsAppCompat module + +test_module_config { + name: "WMShellFlickerTestsAppCompat-CatchAll", + base: "WMShellFlickerTestsAppCompat", + exclude_filters: [ + "com.android.wm.shell.flicker.appcompat.OpenAppInSizeCompatModeTest", + "com.android.wm.shell.flicker.appcompat.OpenTransparentActivityTest", + "com.android.wm.shell.flicker.appcompat.QuickSwitchLauncherToLetterboxAppTest", + "com.android.wm.shell.flicker.appcompat.RepositionFixedPortraitAppTest", + "com.android.wm.shell.flicker.appcompat.RestartAppInSizeCompatModeTest", + "com.android.wm.shell.flicker.appcompat.RotateImmersiveAppInFullscreenTest", + ], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsAppCompat-OpenAppInSizeCompatModeTest", + base: "WMShellFlickerTestsAppCompat", + include_filters: ["com.android.wm.shell.flicker.appcompat.OpenAppInSizeCompatModeTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsAppCompat-OpenTransparentActivityTest", + base: "WMShellFlickerTestsAppCompat", + include_filters: ["com.android.wm.shell.flicker.appcompat.OpenTransparentActivityTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsAppCompat-QuickSwitchLauncherToLetterboxAppTest", + base: "WMShellFlickerTestsAppCompat", + include_filters: ["com.android.wm.shell.flicker.appcompat.QuickSwitchLauncherToLetterboxAppTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsAppCompat-RepositionFixedPortraitAppTest", + base: "WMShellFlickerTestsAppCompat", + include_filters: ["com.android.wm.shell.flicker.appcompat.RepositionFixedPortraitAppTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsAppCompat-RestartAppInSizeCompatModeTest", + base: "WMShellFlickerTestsAppCompat", + include_filters: ["com.android.wm.shell.flicker.appcompat.RestartAppInSizeCompatModeTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsAppCompat-RotateImmersiveAppInFullscreenTest", + base: "WMShellFlickerTestsAppCompat", + include_filters: ["com.android.wm.shell.flicker.appcompat.RotateImmersiveAppInFullscreenTest"], + test_suites: ["device-tests"], +} + +// End breakdowns for FlickerTestsRotation module +//////////////////////////////////////////////////////////////////////////////// diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/OWNERS b/libs/WindowManager/Shell/tests/flicker/appcompat/OWNERS new file mode 100644 index 000000000000..a36a4f85fa4e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/OWNERS @@ -0,0 +1,2 @@ +# Window Manager > App Compat +# Bug component: 970984
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/Android.bp b/libs/WindowManager/Shell/tests/flicker/bubble/Android.bp index f0b4f1faad46..2ff7ab231c28 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/bubble/Android.bp @@ -15,6 +15,7 @@ // package { + default_team: "trendy_team_multitasking_windowing", // See: http://go/android-license-faq // A large-scale-change added 'default_applicable_licenses' to import // all of the 'license_kinds' from "frameworks_base_license" @@ -34,3 +35,57 @@ android_test { static_libs: ["WMShellFlickerTestsBase"], data: ["trace_config/*"], } + +//////////////////////////////////////////////////////////////////////////////// +// Begin breakdowns for WMShellFlickerTestsBubbles module + +test_module_config { + name: "WMShellFlickerTestsBubbles-CatchAll", + base: "WMShellFlickerTestsBubbles", + exclude_filters: [ + "com.android.wm.shell.flicker.bubble.ChangeActiveActivityFromBubbleTest", + "com.android.wm.shell.flicker.bubble.DragToDismissBubbleScreenTest", + "com.android.wm.shell.flicker.bubble.OpenActivityFromBubbleOnLocksreenTest", + "com.android.wm.shell.flicker.bubble.OpenActivityFromBubbleTest", + "com.android.wm.shell.flicker.bubble.SendBubbleNotificationTest", + ], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsBubbles-ChangeActiveActivityFromBubbleTest", + base: "WMShellFlickerTestsBubbles", + include_filters: ["com.android.wm.shell.flicker.bubble.ChangeActiveActivityFromBubbleTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsBubbles-DragToDismissBubbleScreenTest", + base: "WMShellFlickerTestsBubbles", + include_filters: ["com.android.wm.shell.flicker.bubble.DragToDismissBubbleScreenTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsBubbles-OpenActivityFromBubbleOnLocksreenTest", + base: "WMShellFlickerTestsBubbles", + include_filters: ["com.android.wm.shell.flicker.bubble.OpenActivityFromBubbleOnLocksreenTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsBubbles-OpenActivityFromBubbleTest", + base: "WMShellFlickerTestsBubbles", + include_filters: ["com.android.wm.shell.flicker.bubble.OpenActivityFromBubbleTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsBubbles-SendBubbleNotificationTest", + base: "WMShellFlickerTestsBubbles", + include_filters: ["com.android.wm.shell.flicker.bubble.SendBubbleNotificationTest"], + test_suites: ["device-tests"], +} + +// End breakdowns for WMShellFlickerTestsBubbles module +//////////////////////////////////////////////////////////////////////////////// diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt index 2ee53f4fce66..d7ea9f3a8fa4 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt @@ -100,14 +100,14 @@ class OpenActivityFromBubbleOnLocksreenTest(flicker: LegacyFlickerTest) : @Postsubmit @Test fun navBarLayerIsVisibleAtEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.navBarLayerIsVisibleAtEnd() } @Postsubmit @Test fun navBarLayerPositionAtEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.navBarLayerPositionAtEnd() } @@ -154,7 +154,7 @@ class OpenActivityFromBubbleOnLocksreenTest(flicker: LegacyFlickerTest) : @Postsubmit @Test fun taskBarLayerIsVisibleAtEnd() { - Assume.assumeTrue(flicker.scenario.isTablet) + Assume.assumeTrue(usesTaskbar) flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.TASK_BAR) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/pip/Android.bp b/libs/WindowManager/Shell/tests/flicker/pip/Android.bp index faeb342a44be..4165ed093929 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/pip/Android.bp @@ -15,6 +15,7 @@ // package { + default_team: "trendy_team_multitasking_windowing", // See: http://go/android-license-faq // A large-scale-change added 'default_applicable_licenses' to import // all of the 'license_kinds' from "frameworks_base_license" @@ -24,6 +25,14 @@ package { } filegroup { + name: "WMShellFlickerTestsPipApps-src", + srcs: ["src/**/apps/*.kt"], +} + +//////////////////////////////////////////////////////////////////////////////// +// Begin cleanup after gcl merges + +filegroup { name: "WMShellFlickerTestsPip1-src", srcs: [ "src/**/A*.kt", @@ -52,11 +61,6 @@ filegroup { srcs: ["src/**/common/*.kt"], } -filegroup { - name: "WMShellFlickerTestsPipApps-src", - srcs: ["src/**/apps/*.kt"], -} - android_test { name: "WMShellFlickerTestsPip1", defaults: ["WMShellFlickerTestsDefault"], @@ -107,6 +111,21 @@ android_test { data: ["trace_config/*"], } +//////////////////////////////////////////////////////////////////////////////// +// End cleanup after gcl merges + +android_test { + name: "WMShellFlickerTestsPip", + defaults: ["WMShellFlickerTestsDefault"], + manifest: "AndroidManifest.xml", + package_name: "com.android.wm.shell.flicker.pip", + instrumentation_target_package: "com.android.wm.shell.flicker.pip", + test_config_template: "AndroidTestTemplate.xml", + srcs: ["src/**/*.kt"], + static_libs: ["WMShellFlickerTestsBase"], + data: ["trace_config/*"], +} + android_test { name: "WMShellFlickerTestsPipApps", defaults: ["WMShellFlickerTestsDefault"], @@ -146,3 +165,185 @@ csuite_test { test_plan_include: "csuitePlan.xml", test_config_template: "csuiteDefaultTemplate.xml", } + +//////////////////////////////////////////////////////////////////////////////// +// Begin breakdowns for WMShellFlickerTestsPip module + +test_module_config { + name: "WMShellFlickerTestsPip-CatchAll", + base: "WMShellFlickerTestsPip", + exclude_filters: [ + "com.android.wm.shell.flicker.pip.AutoEnterPipOnGoToHomeTest", + "com.android.wm.shell.flicker.pip.AutoEnterPipWithSourceRectHintTest", + "com.android.wm.shell.flicker.pip.ClosePipBySwipingDownTest", + "com.android.wm.shell.flicker.pip.ClosePipWithDismissButtonTest", + "com.android.wm.shell.flicker.pip.EnterPipOnUserLeaveHintTest", + "com.android.wm.shell.flicker.pip.EnterPipViaAppUiButtonTest", + "com.android.wm.shell.flicker.pip.ExitPipToAppViaExpandButtonTest", + "com.android.wm.shell.flicker.pip.ExitPipToAppViaIntentTest", + "com.android.wm.shell.flicker.pip.ExpandPipOnDoubleClickTest", + "com.android.wm.shell.flicker.pip.ExpandPipOnPinchOpenTest", + "com.android.wm.shell.flicker.pip.FromSplitScreenAutoEnterPipOnGoToHomeTest", + "com.android.wm.shell.flicker.pip.FromSplitScreenEnterPipOnUserLeaveHintTest", + "com.android.wm.shell.flicker.pip.MovePipDownOnShelfHeightChange", + "com.android.wm.shell.flicker.pip.MovePipOnImeVisibilityChangeTest", + "com.android.wm.shell.flicker.pip.MovePipUpOnShelfHeightChangeTest", + "com.android.wm.shell.flicker.pip.PipAspectRatioChangeTest", + "com.android.wm.shell.flicker.pip.PipDragTest", + "com.android.wm.shell.flicker.pip.PipDragThenSnapTest", + "com.android.wm.shell.flicker.pip.PipPinchInTest", + "com.android.wm.shell.flicker.pip.SetRequestedOrientationWhilePinned", + "com.android.wm.shell.flicker.pip.ShowPipAndRotateDisplay", + ], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-AutoEnterPipOnGoToHomeTest", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.AutoEnterPipOnGoToHomeTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-AutoEnterPipWithSourceRectHintTest", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.AutoEnterPipWithSourceRectHintTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-ClosePipBySwipingDownTest", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.ClosePipBySwipingDownTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-ClosePipWithDismissButtonTest", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.ClosePipWithDismissButtonTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-EnterPipOnUserLeaveHintTest", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.EnterPipOnUserLeaveHintTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-EnterPipViaAppUiButtonTest", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.EnterPipViaAppUiButtonTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-ExitPipToAppViaExpandButtonTest", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.ExitPipToAppViaExpandButtonTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-ExitPipToAppViaIntentTest", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.ExitPipToAppViaIntentTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-ExpandPipOnDoubleClickTest", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.ExpandPipOnDoubleClickTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-ExpandPipOnPinchOpenTest", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.ExpandPipOnPinchOpenTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-FromSplitScreenAutoEnterPipOnGoToHomeTest", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.FromSplitScreenAutoEnterPipOnGoToHomeTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-FromSplitScreenEnterPipOnUserLeaveHintTest", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.FromSplitScreenEnterPipOnUserLeaveHintTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-MovePipDownOnShelfHeightChange", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.MovePipDownOnShelfHeightChange"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-MovePipOnImeVisibilityChangeTest", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.MovePipOnImeVisibilityChangeTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-MovePipUpOnShelfHeightChangeTest", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.MovePipUpOnShelfHeightChangeTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-PipAspectRatioChangeTest", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.PipAspectRatioChangeTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-PipDragTest", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.PipDragTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-PipDragThenSnapTest", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.PipDragThenSnapTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-PipPinchInTest", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.PipPinchInTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-SetRequestedOrientationWhilePinned", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.SetRequestedOrientationWhilePinned"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "WMShellFlickerTestsPip-ShowPipAndRotateDisplay", + base: "WMShellFlickerTestsPip", + include_filters: ["com.android.wm.shell.flicker.pip.ShowPipAndRotateDisplay"], + test_suites: ["device-tests"], +} + +// End breakdowns for WMShellFlickerTestsPip module +//////////////////////////////////////////////////////////////////////////////// diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt index b85d7936efc2..a9ed13a099f3 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt @@ -16,10 +16,14 @@ package com.android.wm.shell.flicker.pip +import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest +import android.tools.flicker.subject.exceptions.ExceptionMessageBuilder +import android.tools.flicker.subject.exceptions.IncorrectRegionException +import android.tools.flicker.subject.layers.LayerSubject import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.pip.common.EnterPipTransition @@ -29,6 +33,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized +import kotlin.math.abs /** * Test entering pip from an app via auto-enter property when navigating to home. @@ -67,9 +72,24 @@ open class AutoEnterPipOnGoToHomeTest(flicker: LegacyFlickerTest) : EnterPipTran override val defaultTeardown: FlickerBuilder.() -> Unit = { teardown { pipApp.exit(wmHelper) } } - @FlakyTest(bugId = 293133362) + private val widthNotSmallerThan: LayerSubject.(LayerSubject) -> Unit = { + val width = visibleRegion.region.bounds.width() + val otherWidth = it.visibleRegion.region.bounds.width() + if (width < otherWidth && abs(width - otherWidth) > EPSILON) { + val errorMsgBuilder = + ExceptionMessageBuilder() + .forSubject(this) + .forIncorrectRegion("width. $width smaller than $otherWidth") + .setExpected(width) + .setActual(otherWidth) + throw IncorrectRegionException(errorMsgBuilder) + } + } + + @Postsubmit @Test override fun pipLayerReduces() { + Assume.assumeFalse(flicker.scenario.isGesturalNavigation) flicker.assertLayers { val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } pipLayerList.zipWithNext { previous, current -> @@ -78,6 +98,32 @@ open class AutoEnterPipOnGoToHomeTest(flicker: LegacyFlickerTest) : EnterPipTran } } + /** Checks that [pipApp] window's width is first decreasing then increasing. */ + @Postsubmit + @Test + fun pipLayerWidthDecreasesThenIncreases() { + Assume.assumeTrue(flicker.scenario.isGesturalNavigation) + flicker.assertLayers { + val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } + var previousLayer = pipLayerList[0] + var currentLayer = previousLayer + var i = 0 + invoke("layer area is decreasing") { + if (i < pipLayerList.size - 1) { + previousLayer = currentLayer + currentLayer = pipLayerList[++i] + previousLayer.widthNotSmallerThan(currentLayer) + } + }.then().invoke("layer are is increasing", true /* isOptional */) { + if (i < pipLayerList.size - 1) { + previousLayer = currentLayer + currentLayer = pipLayerList[++i] + currentLayer.widthNotSmallerThan(previousLayer) + } + } + } + } + /** Checks that [pipApp] window is animated towards default position in right bottom corner */ @FlakyTest(bugId = 255578530) @Test @@ -108,4 +154,9 @@ open class AutoEnterPipOnGoToHomeTest(flicker: LegacyFlickerTest) : EnterPipTran override fun visibleLayersShownMoreThanOneConsecutiveEntry() { super.visibleLayersShownMoreThanOneConsecutiveEntry() } + + companion object { + // TODO(b/363080056): A margin of error allowed on certain layer size calculations. + const val EPSILON = 1 + } } diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt index a5e0550d9c79..3ffc9d7b87f6 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt @@ -21,6 +21,7 @@ import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.traces.component.ComponentNameMatcher +import com.android.wm.shell.Flags import com.android.wm.shell.flicker.pip.common.ClosePipTransition import org.junit.FixMethodOrder import org.junit.Test @@ -60,7 +61,7 @@ class ClosePipBySwipingDownTest(flicker: LegacyFlickerTest) : ClosePipTransition val pipCenterY = pipRegion.centerY() val displayCenterX = device.displayWidth / 2 val barComponent = - if (flicker.scenario.isTablet) { + if (flicker.scenario.isTablet || Flags.enableTaskbarOnPhones()) { ComponentNameMatcher.TASK_BAR } else { ComponentNameMatcher.NAV_BAR diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt index a0a61fe2cf72..d0e8215e662e 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt @@ -117,12 +117,10 @@ class EnterPipToOtherOrientation(flicker: LegacyFlickerTest) : PipTransition(fli /** * Checks that all parts of the screen are covered at the start and end of the transition - * - * TODO b/197726599 Prevents all states from being checked */ @Presubmit @Test - fun entireScreenCoveredAtStartAndEnd() = flicker.entireScreenCovered(allStates = false) + fun entireScreenCoveredAtStartAndEnd() = flicker.entireScreenCovered() /** Checks [pipApp] window remains visible and on top throughout the transition */ @Presubmit diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt index d03d7799d675..d1bf6acf785d 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt @@ -183,12 +183,6 @@ class FromSplitScreenEnterPipOnUserLeaveHintTest(flicker: LegacyFlickerTest) : } /** {@inheritDoc} */ - @FlakyTest(bugId = 312446524) - @Test - override fun visibleLayersShownMoreThanOneConsecutiveEntry() = - super.visibleLayersShownMoreThanOneConsecutiveEntry() - - /** {@inheritDoc} */ @Test @FlakyTest(bugId = 336510055) override fun entireScreenCovered() { diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipAspectRatioChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipAspectRatioChangeTest.kt index 70be58f06548..429774f890a5 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipAspectRatioChangeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipAspectRatioChangeTest.kt @@ -35,7 +35,7 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) class PipAspectRatioChangeTest(flicker: LegacyFlickerTest) : PipTransition(flicker) { override val thisTransition: FlickerBuilder.() -> Unit = { - transitions { pipApp.changeAspectRatio() } + transitions { pipApp.changeAspectRatio(wmHelper) } } @Presubmit diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt index 90b9798c6329..cbd4a528474a 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipDragThenSnapTest.kt @@ -181,6 +181,13 @@ class PipDragThenSnapTest(flicker: LegacyFlickerTest) : PipTransition(flicker) { super.taskBarWindowIsAlwaysVisible() } + // Overridden to remove @Postsubmit annotation + @Test + @FlakyTest(bugId = 294993100) + override fun pipLayerHasCorrectCornersAtEnd() { + // No rounded corners as we go back to fullscreen in new orientation. + } + companion object { /** * Creates the test configurations. diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt index ed2a0a718c6c..578a9b536289 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt @@ -147,6 +147,12 @@ open class SetRequestedOrientationWhilePinned(flicker: LegacyFlickerTest) : PipT @Test override fun entireScreenCovered() = super.entireScreenCovered() + @Postsubmit + @Test + override fun pipLayerHasCorrectCornersAtEnd() { + flicker.assertLayersEnd { hasNoRoundedCorners(pipApp) } + } + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt index 3a0eeb67995b..68fa7c7af740 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt @@ -103,7 +103,7 @@ open class NetflixEnterPipTest(flicker: LegacyFlickerTest) : AppsEnterPipTransit @Postsubmit @Test override fun taskBarLayerIsVisibleAtStartAndEnd() { - Assume.assumeTrue(flicker.scenario.isTablet) + Assume.assumeTrue(usesTaskbar) // Netflix starts in immersive fullscreen mode, so taskbar bar is not visible at start flicker.assertLayersStart { this.isInvisible(ComponentNameMatcher.TASK_BAR) } flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.TASK_BAR) } diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipTest.kt index 35ed8de3a464..7873a85d515d 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipTest.kt @@ -65,7 +65,7 @@ open class YouTubeEnterPipTest(flicker: LegacyFlickerTest) : AppsEnterPipTransit setup { standardAppHelper.launchViaIntent( wmHelper, - YouTubeAppHelper.getYoutubeVideoIntent("HPcEAtoXXLA"), + YouTubeAppHelper.getYoutubeVideoIntent("3KtWfp0UopM"), ComponentNameMatcher(YouTubeAppHelper.PACKAGE_NAME, "") ) standardAppHelper.waitForVideoPlaying() diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipToOtherOrientationTest.kt index 879034f32514..72be3d85ec8b 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipToOtherOrientationTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipToOtherOrientationTest.kt @@ -73,7 +73,7 @@ open class YouTubeEnterPipToOtherOrientationTest(flicker: LegacyFlickerTest) : setup { standardAppHelper.launchViaIntent( wmHelper, - YouTubeAppHelper.getYoutubeVideoIntent("HPcEAtoXXLA"), + YouTubeAppHelper.getYoutubeVideoIntent("3KtWfp0UopM"), ComponentNameMatcher(YouTubeAppHelper.PACKAGE_NAME, "") ) standardAppHelper.enterFullscreen() @@ -88,7 +88,7 @@ open class YouTubeEnterPipToOtherOrientationTest(flicker: LegacyFlickerTest) : @Postsubmit @Test override fun taskBarLayerIsVisibleAtStartAndEnd() { - Assume.assumeTrue(flicker.scenario.isTablet) + Assume.assumeTrue(usesTaskbar) // YouTube starts in immersive fullscreen mode, so taskbar bar is not visible at start flicker.assertLayersStart { this.isInvisible(ComponentNameMatcher.TASK_BAR) } flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.TASK_BAR) } diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ClosePipTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ClosePipTransition.kt index 8cb81b46cf4d..f57335c2081b 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ClosePipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ClosePipTransition.kt @@ -70,6 +70,11 @@ abstract class ClosePipTransition(flicker: LegacyFlickerTest) : PipTransition(fl } } + @Test + override fun pipLayerHasCorrectCornersAtEnd() { + // PiP might have completely faded out by this point, so corner radii not applicable. + } + companion object { /** * Creates the test configurations. diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ExitPipToAppTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ExitPipToAppTransition.kt index 0742cf9c5887..ce84eb644042 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ExitPipToAppTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ExitPipToAppTransition.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.pip.common +import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.Rotation import android.tools.flicker.legacy.LegacyFlickerTest @@ -123,6 +124,12 @@ abstract class ExitPipToAppTransition(flicker: LegacyFlickerTest) : PipTransitio } } + @Postsubmit + @Test + override fun pipLayerHasCorrectCornersAtEnd() { + flicker.assertLayersEnd { hasNoRoundedCorners(pipApp) } + } + /** {@inheritDoc} */ @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered() diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipTransition.kt index 99c1ad2aaa4e..bc2bfdbe1df1 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipTransition.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.flicker.pip.common import android.app.Instrumentation import android.content.Intent +import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.Rotation import android.tools.flicker.legacy.FlickerBuilder @@ -105,4 +106,10 @@ abstract class PipTransition(flicker: LegacyFlickerTest) : BaseTest(flicker) { .doesNotContain(false) } } + + @Postsubmit + @Test + open fun pipLayerHasCorrectCornersAtEnd() { + flicker.assertLayersEnd { hasRoundedCorners(pipApp) } + } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt deleted file mode 100644 index d485b82f5ddb..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (C) 2024 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.service.desktopmode.flicker - -import android.tools.flicker.AssertionInvocationGroup -import android.tools.flicker.assertors.assertions.AppLayerIsInvisibleAtEnd -import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways -import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAtStart -import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd -import android.tools.flicker.assertors.assertions.AppWindowIsVisibleAlways -import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd -import android.tools.flicker.assertors.assertions.AppWindowOnTopAtStart -import android.tools.flicker.assertors.assertions.AppWindowRemainInsideDisplayBounds -import android.tools.flicker.assertors.assertions.LauncherWindowMovesToTop -import android.tools.flicker.config.AssertionTemplates -import android.tools.flicker.config.FlickerConfigEntry -import android.tools.flicker.config.ScenarioId -import android.tools.flicker.config.desktopmode.Components -import android.tools.flicker.extractors.ITransitionMatcher -import android.tools.flicker.extractors.ShellTransitionScenarioExtractor -import android.tools.traces.wm.Transition -import android.tools.traces.wm.TransitionType - -class DesktopModeFlickerScenarios { - companion object { - val END_DRAG_TO_DESKTOP = - FlickerConfigEntry( - scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"), - extractor = - ShellTransitionScenarioExtractor( - transitionMatcher = - object : ITransitionMatcher { - override fun findAll( - transitions: Collection<Transition> - ): Collection<Transition> { - return transitions.filter { - it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP - } - } - } - ), - assertions = - AssertionTemplates.COMMON_ASSERTIONS + - listOf( - AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP), - AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP), - AppWindowHasDesktopModeInitialBoundsAtTheEnd( - Components.DESKTOP_MODE_APP - ) - ) - .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), - ) - - val CLOSE_APP = - FlickerConfigEntry( - scenarioId = ScenarioId("CLOSE_APP"), - extractor = - ShellTransitionScenarioExtractor( - transitionMatcher = - object : ITransitionMatcher { - override fun findAll( - transitions: Collection<Transition> - ): Collection<Transition> { - return transitions.filter { it.type == TransitionType.CLOSE } - } - } - ), - assertions = - AssertionTemplates.COMMON_ASSERTIONS + - listOf( - AppWindowOnTopAtStart(Components.DESKTOP_MODE_APP), - AppLayerIsVisibleAtStart(Components.DESKTOP_MODE_APP), - AppLayerIsInvisibleAtEnd(Components.DESKTOP_MODE_APP), - ) - .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), - ) - - val CLOSE_LAST_APP = - FlickerConfigEntry( - scenarioId = ScenarioId("CLOSE_LAST_APP"), - extractor = - ShellTransitionScenarioExtractor( - transitionMatcher = - object : ITransitionMatcher { - override fun findAll( - transitions: Collection<Transition> - ): Collection<Transition> { - val lastTransition = - transitions.findLast { it.type == TransitionType.CLOSE } - return if (lastTransition != null) listOf(lastTransition) - else emptyList() - } - } - ), - assertions = - AssertionTemplates.COMMON_ASSERTIONS + - listOf( - AppWindowOnTopAtStart(Components.DESKTOP_MODE_APP), - AppLayerIsVisibleAtStart(Components.DESKTOP_MODE_APP), - AppLayerIsInvisibleAtEnd(Components.DESKTOP_MODE_APP), - LauncherWindowMovesToTop() - ) - .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), - ) - - val CORNER_RESIZE = - FlickerConfigEntry( - scenarioId = ScenarioId("CORNER_RESIZE"), - extractor = - ShellTransitionScenarioExtractor( - transitionMatcher = - object : ITransitionMatcher { - override fun findAll( - transitions: Collection<Transition> - ): Collection<Transition> { - return transitions.filter { - it.type == TransitionType.CHANGE - } - } - } - ), - assertions = - listOf( - AppWindowIsVisibleAlways(Components.DESKTOP_MODE_APP), - AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP), - AppWindowRemainInsideDisplayBounds(Components.DESKTOP_MODE_APP), - ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), - ) - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp b/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp deleted file mode 100644 index 0fe7a16be851..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp +++ /dev/null @@ -1,82 +0,0 @@ -// -// Copyright (C) 2020 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -package { - // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: ["frameworks_base_license"], -} - -filegroup { - name: "WMShellFlickerTestsSplitScreenBase-src", - srcs: [ - "src/**/benchmark/*.kt", - ], -} - -filegroup { - name: "WMShellFlickerTestsSplitScreenGroup1-src", - srcs: [ - "src/**/A*.kt", - "src/**/B*.kt", - "src/**/C*.kt", - "src/**/D*.kt", - "src/**/E*.kt", - ], -} - -filegroup { - name: "WMShellFlickerTestsSplitScreenGroup2-src", - srcs: [ - "src/**/*.kt", - ], -} - -android_test { - name: "WMShellFlickerTestsSplitScreenGroup1", - defaults: ["WMShellFlickerTestsDefault"], - manifest: "AndroidManifest.xml", - package_name: "com.android.wm.shell.flicker.splitscreen", - instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen", - test_config_template: "AndroidTestTemplate.xml", - srcs: [ - ":WMShellFlickerTestsSplitScreenBase-src", - ":WMShellFlickerTestsSplitScreenGroup1-src", - ], - static_libs: ["WMShellFlickerTestsBase"], - data: ["trace_config/*"], -} - -android_test { - name: "WMShellFlickerTestsSplitScreenGroup2", - defaults: ["WMShellFlickerTestsDefault"], - manifest: "AndroidManifest.xml", - package_name: "com.android.wm.shell.flicker.splitscreen", - instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen", - test_config_template: "AndroidTestTemplate.xml", - srcs: [ - ":WMShellFlickerTestsSplitScreenBase-src", - ":WMShellFlickerTestsSplitScreenGroup2-src", - ], - exclude_srcs: [ - ":WMShellFlickerTestsSplitScreenGroup1-src", - ], - static_libs: ["WMShellFlickerTestsBase"], - data: ["trace_config/*"], -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt index 4465a16a8e0f..acaf021981ed 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt @@ -28,12 +28,16 @@ import com.android.server.wm.flicker.statusBarLayerPositionAtStartAndEnd import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible import com.android.server.wm.flicker.taskBarLayerIsVisibleAtStartAndEnd import com.android.server.wm.flicker.taskBarWindowIsAlwaysVisible +import com.android.wm.shell.Flags import org.junit.Assume import org.junit.Test interface ICommonAssertions { val flicker: LegacyFlickerTest + val usesTaskbar: Boolean + get() = flicker.scenario.isTablet || Flags.enableTaskbarOnPhones() + /** Checks that all parts of the screen are covered during the transition */ @Presubmit @Test fun entireScreenCovered() = flicker.entireScreenCovered() @@ -43,7 +47,7 @@ interface ICommonAssertions { @Presubmit @Test fun navBarLayerIsVisibleAtStartAndEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.navBarLayerIsVisibleAtStartAndEnd() } @@ -54,7 +58,7 @@ interface ICommonAssertions { @Presubmit @Test fun navBarLayerPositionAtStartAndEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.navBarLayerPositionAtStartAndEnd() } @@ -66,7 +70,7 @@ interface ICommonAssertions { @Presubmit @Test fun navBarWindowIsAlwaysVisible() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.navBarWindowIsAlwaysVisible() } @@ -76,7 +80,7 @@ interface ICommonAssertions { @Presubmit @Test fun taskBarLayerIsVisibleAtStartAndEnd() { - Assume.assumeTrue(flicker.scenario.isTablet) + Assume.assumeTrue(usesTaskbar) flicker.taskBarLayerIsVisibleAtStartAndEnd() } @@ -88,7 +92,7 @@ interface ICommonAssertions { @Presubmit @Test fun taskBarWindowIsAlwaysVisible() { - Assume.assumeTrue(flicker.scenario.isTablet) + Assume.assumeTrue(usesTaskbar) flicker.taskBarWindowIsAlwaysVisible() } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionService.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionService.kt new file mode 100644 index 000000000000..aa4e216f01a2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionService.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2024 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.utils + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.drawable.Icon +import android.os.IBinder +import android.os.Message +import android.os.Messenger +import android.os.RemoteException +import android.util.Log + +class MediaProjectionService : Service() { + + private var mTestBitmap: Bitmap? = null + + private val notificationId: Int = 1 + private val notificationChannelId: String = "MediaProjectionFlickerTest" + private val notificationChannelName = "FlickerMediaProjectionService" + + var mMessenger: Messenger? = null + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + mMessenger = intent.extras?.getParcelable( + MediaProjectionUtils.EXTRA_MESSENGER, Messenger::class.java) + startForeground() + return super.onStartCommand(intent, flags, startId) + } + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onDestroy() { + mTestBitmap?.recycle() + mTestBitmap = null + sendMessage(MediaProjectionUtils.MSG_SERVICE_DESTROYED) + super.onDestroy() + } + + private fun createNotificationIcon(): Icon { + Log.d(TAG, "createNotification") + + mTestBitmap = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888) + val canvas = Canvas(mTestBitmap!!) + canvas.drawColor(Color.BLUE) + return Icon.createWithBitmap(mTestBitmap) + } + + private fun startForeground() { + Log.d(TAG, "startForeground") + val channel = NotificationChannel( + notificationChannelId, + notificationChannelName, NotificationManager.IMPORTANCE_NONE + ) + channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE + + val notificationManager: NotificationManager = + getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + + val notificationBuilder: Notification.Builder = + Notification.Builder(this, notificationChannelId) + + val notification = notificationBuilder.setOngoing(true) + .setContentTitle("App is running") + .setSmallIcon(createNotificationIcon()) + .setCategory(Notification.CATEGORY_SERVICE) + .setContentText("Context") + .build() + + startForeground(notificationId, notification) + sendMessage(MediaProjectionUtils.MSG_START_FOREGROUND_DONE) + } + + fun sendMessage(what: Int) { + Log.d(TAG, "sendMessage") + with(Message.obtain()) { + this.what = what + try { + mMessenger!!.send(this) + } catch (e: RemoteException) { + Log.d(TAG, "Unable to send message", e) + } + } + } + + companion object { + private const val TAG: String = "FlickerMediaProjectionService" + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionUtils.kt new file mode 100644 index 000000000000..f9706969ff11 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionUtils.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 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.utils + +object MediaProjectionUtils { + const val REQUEST_CODE: Int = 99 + const val MSG_START_FOREGROUND_DONE: Int = 1 + const val MSG_SERVICE_DESTROYED: Int = 2 + const val EXTRA_MESSENGER: String = "messenger" +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp index 787c9f332b2d..049a5a0615e0 100644 --- a/libs/WindowManager/Shell/tests/unittest/Android.bp +++ b/libs/WindowManager/Shell/tests/unittest/Android.bp @@ -37,15 +37,19 @@ android_test { ], static_libs: [ + "TestParameterInjector", "WindowManager-Shell", "junit", "flag-junit", "androidx.test.runner", "androidx.test.rules", "androidx.test.ext.junit", + "androidx.datastore_datastore", + "kotlinx_coroutines_test", "androidx.dynamicanimation_dynamicanimation", "dagger2", "frameworks-base-testutils", + "kotlin-test", "kotlinx-coroutines-android", "kotlinx-coroutines-core", "mockito-kotlin2", @@ -58,6 +62,7 @@ android_test { "guava-android-testlib", "com.android.window.flags.window-aconfig-java", "platform-test-annotations", + "flag-junit", ], libs: [ diff --git a/libs/WindowManager/Shell/tests/unittest/res/layout/caption_layout.xml b/libs/WindowManager/Shell/tests/unittest/res/layout/caption_layout.xml new file mode 100644 index 000000000000..079ee13ba4da --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/res/layout/caption_layout.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 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.windowdecor.WindowDecorLinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="end" + android:background="@drawable/caption_decor_title"/>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java index f9b4108bc8c2..f01ed84adc74 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -16,10 +16,6 @@ package com.android.wm.shell; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; @@ -43,8 +39,10 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.never; import android.app.ActivityManager.RunningTaskInfo; +import android.app.TaskInfo; import android.content.LocusId; import android.content.pm.ParceledListSlice; import android.os.Binder; @@ -52,7 +50,6 @@ import android.os.IBinder; import android.os.RemoteException; import android.util.SparseArray; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.window.ITaskOrganizer; import android.window.ITaskOrganizerController; import android.window.TaskAppearedInfo; @@ -63,6 +60,8 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.compatui.CompatUIController; +import com.android.wm.shell.compatui.api.CompatUIInfo; +import com.android.wm.shell.compatui.impl.CompatUIEvents; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellInit; @@ -70,6 +69,7 @@ import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -169,7 +169,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { public void testTaskLeashReleasedAfterVanished() throws RemoteException { assumeFalse(ENABLE_SHELL_TRANSITIONS); RunningTaskInfo taskInfo = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW); - SurfaceControl taskLeash = new SurfaceControl.Builder(new SurfaceSession()) + SurfaceControl taskLeash = new SurfaceControl.Builder() .setName("task").build(); mOrganizer.registerOrganizer(); mOrganizer.onTaskAppeared(taskInfo, taskLeash); @@ -365,37 +365,37 @@ public class ShellTaskOrganizerTests extends ShellTestCase { final RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 12, WINDOWING_MODE_FULLSCREEN); taskInfo1.displayId = DEFAULT_DISPLAY; - taskInfo1.appCompatTaskInfo.topActivityInSizeCompat = false; + taskInfo1.appCompatTaskInfo.setTopActivityInSizeCompat(false); final TrackingTaskListener taskListener = new TrackingTaskListener(); mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN); mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null); // sizeCompatActivity is null if top activity is not in size compat. - verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */); + verifyOnCompatInfoChangedInvokedWith(taskInfo1, null /* taskListener */); // sizeCompatActivity is non-null if top activity is in size compat. clearInvocations(mCompatUI); final RunningTaskInfo taskInfo2 = createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); taskInfo2.displayId = taskInfo1.displayId; - taskInfo2.appCompatTaskInfo.topActivityInSizeCompat = true; + taskInfo2.appCompatTaskInfo.setTopActivityInSizeCompat(true); taskInfo2.isVisible = true; mOrganizer.onTaskInfoChanged(taskInfo2); - verify(mCompatUI).onCompatInfoChanged(taskInfo2, taskListener); + verifyOnCompatInfoChangedInvokedWith(taskInfo2, taskListener); // Not show size compat UI if task is not visible. clearInvocations(mCompatUI); final RunningTaskInfo taskInfo3 = createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); taskInfo3.displayId = taskInfo1.displayId; - taskInfo3.appCompatTaskInfo.topActivityInSizeCompat = true; + taskInfo3.appCompatTaskInfo.setTopActivityInSizeCompat(true); taskInfo3.isVisible = false; mOrganizer.onTaskInfoChanged(taskInfo3); - verify(mCompatUI).onCompatInfoChanged(taskInfo3, null /* taskListener */); + verifyOnCompatInfoChangedInvokedWith(taskInfo3, null /* taskListener */); clearInvocations(mCompatUI); mOrganizer.onTaskVanished(taskInfo1); - verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */); + verifyOnCompatInfoChangedInvokedWith(taskInfo1, null /* taskListener */); } @Test @@ -403,14 +403,14 @@ public class ShellTaskOrganizerTests extends ShellTestCase { final RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 12, WINDOWING_MODE_FULLSCREEN); taskInfo1.displayId = DEFAULT_DISPLAY; - taskInfo1.appCompatTaskInfo.topActivityEligibleForLetterboxEducation = false; + taskInfo1.appCompatTaskInfo.setEligibleForLetterboxEducation(false); final TrackingTaskListener taskListener = new TrackingTaskListener(); mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN); mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null); // Task listener sent to compat UI is null if top activity isn't eligible for letterbox // education. - verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */); + verifyOnCompatInfoChangedInvokedWith(taskInfo1, null /* taskListener */); // Task listener is non-null if top activity is eligible for letterbox education and task // is visible. @@ -418,102 +418,24 @@ public class ShellTaskOrganizerTests extends ShellTestCase { final RunningTaskInfo taskInfo2 = createTaskInfo(taskInfo1.taskId, WINDOWING_MODE_FULLSCREEN); taskInfo2.displayId = taskInfo1.displayId; - taskInfo2.appCompatTaskInfo.topActivityEligibleForLetterboxEducation = true; + taskInfo2.appCompatTaskInfo.setEligibleForLetterboxEducation(true); taskInfo2.isVisible = true; mOrganizer.onTaskInfoChanged(taskInfo2); - verify(mCompatUI).onCompatInfoChanged(taskInfo2, taskListener); + verifyOnCompatInfoChangedInvokedWith(taskInfo2, taskListener); // Task listener is null if task is invisible. clearInvocations(mCompatUI); final RunningTaskInfo taskInfo3 = createTaskInfo(taskInfo1.taskId, WINDOWING_MODE_FULLSCREEN); taskInfo3.displayId = taskInfo1.displayId; - taskInfo3.appCompatTaskInfo.topActivityEligibleForLetterboxEducation = true; + taskInfo3.appCompatTaskInfo.setEligibleForLetterboxEducation(true); taskInfo3.isVisible = false; mOrganizer.onTaskInfoChanged(taskInfo3); - verify(mCompatUI).onCompatInfoChanged(taskInfo3, null /* taskListener */); + verifyOnCompatInfoChangedInvokedWith(taskInfo3, null /* taskListener */); clearInvocations(mCompatUI); mOrganizer.onTaskVanished(taskInfo1); - verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */); - } - - @Test - public void testOnCameraCompatActivityChanged() { - final RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 1, - WINDOWING_MODE_FULLSCREEN); - taskInfo1.displayId = DEFAULT_DISPLAY; - taskInfo1.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = - CAMERA_COMPAT_CONTROL_HIDDEN; - final TrackingTaskListener taskListener = new TrackingTaskListener(); - mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN); - mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null); - - // Task listener sent to compat UI is null if top activity doesn't request a camera - // compat control. - verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */); - - // Task listener is non-null when request a camera compat control for a visible task. - clearInvocations(mCompatUI); - final RunningTaskInfo taskInfo2 = - createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); - taskInfo2.displayId = taskInfo1.displayId; - taskInfo2.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = - CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; - taskInfo2.isVisible = true; - mOrganizer.onTaskInfoChanged(taskInfo2); - verify(mCompatUI).onCompatInfoChanged(taskInfo2, taskListener); - - // CompatUIController#onCompatInfoChanged is called when requested state for a camera - // compat control changes for a visible task. - clearInvocations(mCompatUI); - final RunningTaskInfo taskInfo3 = - createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); - taskInfo3.displayId = taskInfo1.displayId; - taskInfo3.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = - CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; - taskInfo3.isVisible = true; - mOrganizer.onTaskInfoChanged(taskInfo3); - verify(mCompatUI).onCompatInfoChanged(taskInfo3, taskListener); - - // CompatUIController#onCompatInfoChanged is called when a top activity goes in size compat - // mode for a visible task that has a compat control. - clearInvocations(mCompatUI); - final RunningTaskInfo taskInfo4 = - createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); - taskInfo4.displayId = taskInfo1.displayId; - taskInfo4.appCompatTaskInfo.topActivityInSizeCompat = true; - taskInfo4.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = - CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; - taskInfo4.isVisible = true; - mOrganizer.onTaskInfoChanged(taskInfo4); - verify(mCompatUI).onCompatInfoChanged(taskInfo4, taskListener); - - // Task linster is null when a camera compat control is dimissed for a visible task. - clearInvocations(mCompatUI); - final RunningTaskInfo taskInfo5 = - createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); - taskInfo5.displayId = taskInfo1.displayId; - taskInfo5.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = - CAMERA_COMPAT_CONTROL_DISMISSED; - taskInfo5.isVisible = true; - mOrganizer.onTaskInfoChanged(taskInfo5); - verify(mCompatUI).onCompatInfoChanged(taskInfo5, null /* taskListener */); - - // Task linster is null when request a camera compat control for a invisible task. - clearInvocations(mCompatUI); - final RunningTaskInfo taskInfo6 = - createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); - taskInfo6.displayId = taskInfo1.displayId; - taskInfo6.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = - CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; - taskInfo6.isVisible = false; - mOrganizer.onTaskInfoChanged(taskInfo6); - verify(mCompatUI).onCompatInfoChanged(taskInfo6, null /* taskListener */); - - clearInvocations(mCompatUI); - mOrganizer.onTaskVanished(taskInfo1); - verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */); + verifyOnCompatInfoChangedInvokedWith(taskInfo1, null /* taskListener */); } @Test @@ -640,7 +562,8 @@ public class ShellTaskOrganizerTests extends ShellTestCase { mOrganizer.onTaskAppeared(task1, /* leash= */ null); - mOrganizer.onSizeCompatRestartButtonClicked(task1.taskId); + mOrganizer.onSizeCompatRestartButtonClicked( + new CompatUIEvents.SizeCompatRestartButtonClicked(task1.taskId)); verify(mTaskOrganizerController).restartTaskTopActivityProcessIfVisible(task1.token); } @@ -677,6 +600,18 @@ public class ShellTaskOrganizerTests extends ShellTestCase { } @Test + public void testRecentTasks_visibilityChanges_notFreeForm_shouldNotNotifyTaskController() { + RunningTaskInfo task1_visible = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN); + mOrganizer.onTaskAppeared(task1_visible, /* leash= */ null); + RunningTaskInfo task1_hidden = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN); + task1_hidden.isVisible = false; + + mOrganizer.onTaskInfoChanged(task1_hidden); + + verify(mRecentTasksController, never()).onTaskRunningInfoChanged(task1_hidden); + } + + @Test public void testRecentTasks_windowingModeChanges_shouldNotifyTaskController() { RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN); mOrganizer.onTaskAppeared(task1, /* leash= */ null); @@ -687,6 +622,25 @@ public class ShellTaskOrganizerTests extends ShellTestCase { verify(mRecentTasksController).onTaskRunningInfoChanged(task2); } + @Test + public void testTaskVanishedCallback() { + RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN); + mOrganizer.onTaskAppeared(task1, /* leash= */ null); + + RunningTaskInfo[] vanishedTasks = new RunningTaskInfo[1]; + ShellTaskOrganizer.TaskVanishedListener listener = + new ShellTaskOrganizer.TaskVanishedListener() { + @Override + public void onTaskVanished(RunningTaskInfo taskInfo) { + vanishedTasks[0] = taskInfo; + } + }; + mOrganizer.addTaskVanishedListener(listener); + mOrganizer.onTaskVanished(task1); + + assertEquals(vanishedTasks[0], task1); + } + private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; @@ -694,4 +648,13 @@ public class ShellTaskOrganizerTests extends ShellTestCase { taskInfo.isVisible = true; return taskInfo; } + + private void verifyOnCompatInfoChangedInvokedWith(TaskInfo taskInfo, + ShellTaskOrganizer.TaskListener listener) { + final ArgumentCaptor<CompatUIInfo> capture = ArgumentCaptor.forClass(CompatUIInfo.class); + verify(mCompatUI).onCompatInfoChanged(capture.capture()); + final CompatUIInfo captureValue = capture.getValue(); + assertEquals(captureValue.getTaskInfo(), taskInfo); + assertEquals(captureValue.getListener(), listener); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java index 51a20ee9d090..a2df22c5468f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java @@ -26,7 +26,7 @@ import android.testing.TestableContext; import androidx.test.platform.app.InstrumentationRegistry; -import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.protolog.ProtoLog; import org.junit.After; import org.junit.Before; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java index 24f4d92af9d7..e6bd05b82be9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java @@ -47,6 +47,8 @@ public final class TestRunningTaskInfoBuilder { private ActivityManager.TaskDescription.Builder mTaskDescriptionBuilder = null; private final Point mPositionInParent = new Point(); private boolean mIsVisible = false; + private boolean mIsTopActivityTransparent = false; + private int mNumActivities = 1; private long mLastActiveTime; public static WindowContainerToken createMockWCToken() { @@ -113,6 +115,16 @@ public final class TestRunningTaskInfoBuilder { return this; } + public TestRunningTaskInfoBuilder setTopActivityTransparent(boolean isTopActivityTransparent) { + mIsTopActivityTransparent = isTopActivityTransparent; + return this; + } + + public TestRunningTaskInfoBuilder setNumActivities(int numActivities) { + mNumActivities = numActivities; + return this; + } + public TestRunningTaskInfoBuilder setLastActiveTime(long lastActiveTime) { mLastActiveTime = lastActiveTime; return this; @@ -134,6 +146,8 @@ public final class TestRunningTaskInfoBuilder { mTaskDescriptionBuilder != null ? mTaskDescriptionBuilder.build() : null; info.positionInParent = mPositionInParent; info.isVisible = mIsVisible; + info.isTopActivityTransparent = mIsTopActivityTransparent; + info.numActivities = mNumActivities; info.lastActiveTime = mLastActiveTime; return info; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java index 499870220190..f31722d3c1a5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java @@ -19,6 +19,7 @@ package com.android.wm.shell; import com.android.wm.shell.common.ShellExecutor; import java.util.ArrayList; +import java.util.List; /** * Really basic test executor. It just gathers all events in a blob. The only option is to @@ -52,4 +53,9 @@ public class TestShellExecutor implements ShellExecutor { mRunnables.remove(0).run(); } } + + /** Returns the list of callbacks for this executor. */ + public List<Runnable> getCallbacks() { + return mRunnables; + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java index bd20c1143262..bba9418db66a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java @@ -20,9 +20,13 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; +import static com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationRunner.calculateParentBounds; +import static com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationRunner.shouldUseJumpCutForAnimation; import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; @@ -32,17 +36,24 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import android.animation.Animator; +import android.annotation.NonNull; +import android.graphics.Point; +import android.graphics.Rect; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; import android.window.TransitionInfo; -import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.window.flags.Flags; import com.android.wm.shell.transition.TransitionInfoBuilder; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -50,6 +61,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import java.util.ArrayList; +import java.util.Arrays; /** * Tests for {@link ActivityEmbeddingAnimationRunner}. @@ -58,7 +70,7 @@ import java.util.ArrayList; * atest WMShellUnitTests:ActivityEmbeddingAnimationRunnerTests */ @SmallTest -@RunWith(AndroidJUnit4.class) +@RunWith(TestParameterInjector.class) public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnimationTestBase { @Rule @@ -130,7 +142,7 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim @Test public void testInvalidCustomAnimation_disableAnimationOptionsPerChange() { final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) - .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) + .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY, TRANSIT_OPEN)) .build(); info.setAnimationOptions(TransitionInfo.AnimationOptions .makeCustomAnimOptions("packageName", 0 /* enterResId */, 0 /* exitResId */, @@ -148,7 +160,7 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim @Test public void testInvalidCustomAnimation_enableAnimationOptionsPerChange() { final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) - .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) + .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY, TRANSIT_OPEN)) .build(); info.getChanges().getFirst().setAnimationOptions(TransitionInfo.AnimationOptions .makeCustomAnimOptions("packageName", 0 /* enterResId */, 0 /* exitResId */, @@ -161,4 +173,149 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim // An invalid custom animation is equivalent to jump-cut. assertEquals(0, animator.getDuration()); } + + @DisableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG) + @Test + public void testCalculateParentBounds_flagDisabled() { + final Rect parentBounds = new Rect(0, 0, 2000, 2000); + final Rect primaryBounds = new Rect(); + final Rect secondaryBounds = new Rect(); + parentBounds.splitVertically(primaryBounds, secondaryBounds); + + final TransitionInfo.Change change = createChange(0 /* flags */); + change.setStartAbsBounds(secondaryBounds); + + final TransitionInfo.Change boundsAnimationChange = createChange(0 /* flags */); + boundsAnimationChange.setStartAbsBounds(primaryBounds); + boundsAnimationChange.setEndAbsBounds(primaryBounds); + final Rect actualParentBounds = new Rect(); + + calculateParentBounds(change, boundsAnimationChange, actualParentBounds); + + assertEquals(parentBounds, actualParentBounds); + + actualParentBounds.setEmpty(); + + boundsAnimationChange.setStartAbsBounds(secondaryBounds); + boundsAnimationChange.setEndAbsBounds(primaryBounds); + + calculateParentBounds(boundsAnimationChange, boundsAnimationChange, actualParentBounds); + + assertEquals(parentBounds, actualParentBounds); + } + + // TODO(b/243518738): Rewrite with TestParameter + @EnableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG) + @Test + public void testCalculateParentBounds_flagEnabled_emptyParentSize() { + TransitionInfo.Change change; + final TransitionInfo.Change stubChange = createChange(0 /* flags */); + final Rect actualParentBounds = new Rect(); + change = prepareChangeForParentBoundsCalculationTest( + new Point(0, 0) /* endRelOffset */, + new Rect(0, 0, 2000, 2000), + new Point() /* endParentSize */ + ); + + calculateParentBounds(change, stubChange, actualParentBounds); + + assertTrue("Parent bounds must be empty because end parent size is not set.", + actualParentBounds.isEmpty()); + } + + @EnableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG) + @Test + public void testCalculateParentBounds_flagEnabled( + @TestParameter ParentBoundsTestParameters params) { + final TransitionInfo.Change stubChange = createChange(0 /*flags*/); + final Rect parentBounds = params.getParentBounds(); + final Rect endAbsBounds = params.getEndAbsBounds(); + final TransitionInfo.Change change = prepareChangeForParentBoundsCalculationTest( + new Point(endAbsBounds.left - parentBounds.left, + endAbsBounds.top - parentBounds.top), + endAbsBounds, new Point(parentBounds.width(), parentBounds.height())); + final Rect actualParentBounds = new Rect(); + + calculateParentBounds(change, stubChange, actualParentBounds); + + assertEquals(parentBounds, actualParentBounds); + } + + private enum ParentBoundsTestParameters { + PARENT_START_WITH_0_0( + new int[]{0, 0, 2000, 2000}, + new int[]{0, 0, 2000, 2000}), + CONTAINER_NOT_START_WITH_0_0( + new int[] {0, 0, 2000, 2000}, + new int[] {1000, 500, 1500, 1500}), + PARENT_ON_THE_RIGHT( + new int[] {1000, 0, 2000, 2000}, + new int[] {1000, 500, 1500, 1500}), + PARENT_ON_THE_BOTTOM( + new int[] {0, 1000, 2000, 2000}, + new int[] {500, 1500, 1500, 2000}), + PARENT_IN_THE_MIDDLE( + new int[] {500, 500, 1500, 1500}, + new int[] {1000, 500, 1500, 1000}); + + /** + * An int array to present {left, top, right, bottom} of the parent {@link Rect bounds}. + */ + @NonNull + private final int[] mParentBounds; + + /** + * An int array to present {left, top, right, bottom} of the absolute container + * {@link Rect bounds} after the transition finishes. + */ + @NonNull + private final int[] mEndAbsBounds; + + ParentBoundsTestParameters( + @NonNull int[] parentBounds, @NonNull int[] endAbsBounds) { + mParentBounds = parentBounds; + mEndAbsBounds = endAbsBounds; + } + + @NonNull + private Rect getParentBounds() { + return asRect(mParentBounds); + } + + @NonNull + private Rect getEndAbsBounds() { + return asRect(mEndAbsBounds); + } + + @NonNull + private static Rect asRect(@NonNull int[] bounds) { + if (bounds.length != 4) { + throw new IllegalArgumentException("There must be exactly 4 elements in bounds, " + + "but found " + bounds.length + ": " + Arrays.toString(bounds)); + } + return new Rect(bounds[0], bounds[1], bounds[2], bounds[3]); + } + } + + @Test + public void testShouldUseJumpCutForAnimation() { + final Animation noopAnimation = new AlphaAnimation(0f, 1f); + assertTrue("Animation without duration should use jump cut.", + shouldUseJumpCutForAnimation(noopAnimation)); + + final Animation alphaAnimation = new AlphaAnimation(0f, 1f); + alphaAnimation.setDuration(100); + assertFalse("Animation with duration should not use jump cut.", + shouldUseJumpCutForAnimation(alphaAnimation)); + } + + @NonNull + private static TransitionInfo.Change prepareChangeForParentBoundsCalculationTest( + @NonNull Point endRelOffset, @NonNull Rect endAbsBounds, @NonNull Point endParentSize) { + final TransitionInfo.Change change = createChange(0 /* flags */); + change.setEndRelOffset(endRelOffset.x, endRelOffset.y); + change.setEndAbsBounds(endAbsBounds); + change.setEndParentSize(endParentSize.x, endParentSize.y); + return change; + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java index 0b2265d4ce9c..c18d7ec821b6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java @@ -16,6 +16,7 @@ package com.android.wm.shell.activityembedding; +import static android.view.WindowManager.TRANSIT_NONE; import static android.window.TransitionInfo.FLAG_FILLS_TASK; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; @@ -31,6 +32,7 @@ import android.annotation.NonNull; import android.graphics.Rect; import android.os.IBinder; import android.view.SurfaceControl; +import android.view.WindowManager; import android.window.TransitionInfo; import android.window.WindowContainerToken; @@ -82,11 +84,27 @@ abstract class ActivityEmbeddingAnimationTestBase extends ShellTestCase { spyOn(mFinishCallback); } - /** Creates a mock {@link TransitionInfo.Change}. */ + /** + * Creates a mock {@link TransitionInfo.Change}. + * + * @param flags the {@link TransitionInfo.ChangeFlags} of the change + */ static TransitionInfo.Change createChange(@TransitionInfo.ChangeFlags int flags) { + return createChange(flags, TRANSIT_NONE); + } + + /** + * Creates a mock {@link TransitionInfo.Change}. + * + * @param flags the {@link TransitionInfo.ChangeFlags} of the change + * @param mode the transition mode of the change + */ + static TransitionInfo.Change createChange(@TransitionInfo.ChangeFlags int flags, + @WindowManager.TransitionType int mode) { TransitionInfo.Change c = new TransitionInfo.Change(mock(WindowContainerToken.class), mock(SurfaceControl.class)); c.setFlags(flags); + c.setMode(mode); return c; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/OWNERS new file mode 100644 index 000000000000..622f837dd2dc --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 1168918 +# includes OWNERS from parent directories
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apptoweb/AppToWebGenericLinksParserTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apptoweb/AppToWebGenericLinksParserTests.kt new file mode 100644 index 000000000000..053027fc1cf4 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apptoweb/AppToWebGenericLinksParserTests.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2024 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.apptoweb + +import android.provider.DeviceConfig +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.TestableResources +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.wm.shell.R +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser.Companion.FLAG_GENERIC_LINKS +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNull +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.quality.Strictness + +/** + * Tests for [AppToWebGenericLinksParser]. + * + * Build/Install/Run: atest WMShellUnitTests:AppToWebGenericLinksParserTests + */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner::class) +class AppToWebGenericLinksParserTests : ShellTestCase() { + @Mock private lateinit var mockExecutor: ShellExecutor + + private lateinit var genericLinksParser: AppToWebGenericLinksParser + private lateinit var mockitoSession: StaticMockitoSession + private lateinit var resources: TestableResources + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + mockitoSession = + mockitoSession() + .strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java) + .startMocking() + resources = mContext.getOrCreateTestableResources() + resources.addOverride(R.string.generic_links_list, BUILD_TIME_LIST) + DeviceConfig.setProperty( + NAMESPACE, + FLAG_GENERIC_LINKS, + SERVER_SIDE_LIST, + false /* makeDefault */ + ) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + } + + @Test + fun init_usingBuildTimeList() { + doReturn(true).`when` { DesktopModeStatus.useAppToWebBuildTimeGenericLinks() } + genericLinksParser = AppToWebGenericLinksParser(mContext, mockExecutor) + // Assert build-time list correctly parsed + assertEquals(URL_B, genericLinksParser.getGenericLink(PACKAGE_NAME_1)) + } + + @Test + fun init_usingServerSideList() { + doReturn(false).`when` { DesktopModeStatus.useAppToWebBuildTimeGenericLinks() } + genericLinksParser = AppToWebGenericLinksParser(mContext, mockExecutor) + // Assert server side list correctly parsed + assertEquals(URL_S, genericLinksParser.getGenericLink(PACKAGE_NAME_1)) + } + + @Test + fun init_ignoresMalformedPair() { + doReturn(true).`when` { DesktopModeStatus.useAppToWebBuildTimeGenericLinks() } + val packageName2 = "com.google.android.slides" + val url2 = "https://docs.google.com" + resources.addOverride(R.string.generic_links_list, + "$PACKAGE_NAME_1:$URL_B error $packageName2:$url2") + genericLinksParser = AppToWebGenericLinksParser(mContext, mockExecutor) + // Assert generics links list correctly parsed + assertEquals(URL_B, genericLinksParser.getGenericLink(PACKAGE_NAME_1)) + assertEquals(url2, genericLinksParser.getGenericLink(packageName2)) + } + + + @Test + fun onlySavesValidPackageToUrlMaps() { + doReturn(true).`when` { DesktopModeStatus.useAppToWebBuildTimeGenericLinks() } + resources.addOverride(R.string.generic_links_list, "$PACKAGE_NAME_1:www.yout") + genericLinksParser = AppToWebGenericLinksParser(mContext, mockExecutor) + // Verify map with invalid url not saved + assertNull(genericLinksParser.getGenericLink(PACKAGE_NAME_1)) + } + + companion object { + private const val PACKAGE_NAME_1 = "com.google.android.youtube" + + private const val URL_B = "http://www.youtube.com" + private const val URL_S = "http://www.google.com" + + private const val SERVER_SIDE_LIST = "$PACKAGE_NAME_1:$URL_S" + private const val BUILD_TIME_LIST = "$PACKAGE_NAME_1:$URL_B" + + private const val NAMESPACE = DeviceConfig.NAMESPACE_APP_COMPAT_OVERRIDES + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apptoweb/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apptoweb/OWNERS new file mode 100644 index 000000000000..553540cbb86c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apptoweb/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 929241 +# includes OWNERS from parent directories
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index 57e469d5cbd2..227060d15640 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -16,8 +16,19 @@ package com.android.wm.shell.back; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; import static android.window.BackNavigationInfo.KEY_NAVIGATION_FINISHED; +import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; +import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -25,6 +36,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; @@ -32,6 +44,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import android.app.ActivityManager; import android.app.IActivityTaskManager; import android.app.WindowConfiguration; import android.content.pm.ApplicationInfo; @@ -40,6 +53,7 @@ import android.graphics.Rect; import android.hardware.input.InputManager; import android.os.Bundle; import android.os.Handler; +import android.os.IBinder; import android.os.RemoteCallback; import android.os.RemoteException; import android.provider.Settings; @@ -51,11 +65,16 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; +import android.view.WindowManager; import android.window.BackEvent; import android.window.BackMotionEvent; import android.window.BackNavigationInfo; import android.window.IBackAnimationFinishedCallback; import android.window.IOnBackInvokedCallback; +import android.window.IWindowContainerToken; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; @@ -64,10 +83,11 @@ import com.android.internal.util.test.FakeSettingsProvider; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.shared.ShellSharedConstants; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; -import com.android.wm.shell.sysui.ShellSharedConstants; +import com.android.wm.shell.transition.Transitions; import org.junit.Before; import org.junit.Test; @@ -114,7 +134,11 @@ public class BackAnimationControllerTest extends ShellTestCase { @Mock private ShellCommandHandler mShellCommandHandler; @Mock + private Transitions mTransitions; + @Mock private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; + @Mock + private Handler mHandler; private BackAnimationController mController; private TestableContentResolver mContentResolver; @@ -125,6 +149,8 @@ public class BackAnimationControllerTest extends ShellTestCase { private ShellBackAnimationRegistry mShellBackAnimationRegistry; private Rect mTouchableRegion; + private BackAnimationController.BackTransitionHandler mBackTransitionHandler; + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -137,13 +163,14 @@ public class BackAnimationControllerTest extends ShellTestCase { mTestableLooper = TestableLooper.get(this); mShellInit = spy(new ShellInit(mShellExecutor)); mDefaultCrossActivityBackAnimation = new DefaultCrossActivityBackAnimation(mContext, - mAnimationBackground, mRootTaskDisplayAreaOrganizer); - mCrossTaskBackAnimation = new CrossTaskBackAnimation(mContext, mAnimationBackground); + mAnimationBackground, mRootTaskDisplayAreaOrganizer, mHandler); + mCrossTaskBackAnimation = new CrossTaskBackAnimation(mContext, mAnimationBackground, + mHandler); mShellBackAnimationRegistry = new ShellBackAnimationRegistry(mDefaultCrossActivityBackAnimation, mCrossTaskBackAnimation, /* dialogCloseAnimation= */ null, new CustomCrossActivityBackAnimation(mContext, mAnimationBackground, - mRootTaskDisplayAreaOrganizer), + mRootTaskDisplayAreaOrganizer, mHandler), /* defaultBackToHomeAnimation= */ null); mController = new BackAnimationController( @@ -156,11 +183,15 @@ public class BackAnimationControllerTest extends ShellTestCase { mContentResolver, mAnimationBackground, mShellBackAnimationRegistry, - mShellCommandHandler); + mShellCommandHandler, + mTransitions, + mHandler); mShellInit.init(); mShellExecutor.flushAll(); mTouchableRegion = new Rect(0, 0, 100, 100); mController.mTouchableArea.set(mTouchableRegion); + mBackTransitionHandler = mController.mBackTransitionHandler; + spyOn(mBackTransitionHandler); } private void createNavigationInfo(int backType, @@ -316,7 +347,9 @@ public class BackAnimationControllerTest extends ShellTestCase { mContentResolver, mAnimationBackground, mShellBackAnimationRegistry, - mShellCommandHandler); + mShellCommandHandler, + mTransitions, + mHandler); shellInit.init(); registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME); @@ -601,6 +634,199 @@ public class BackAnimationControllerTest extends ShellTestCase { mCrossTaskBackAnimation.getRunner()); } + @Test + public void testCloseAsExpectTransition() { + final int openTaskId = 1; + final int closeTaskId = 2; + mController.mApps = createAppAnimationTargets(openTaskId, closeTaskId); + final IBinder mockBinder = mock(IBinder.class); + final SurfaceControl.Transaction st = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction ft = mock(SurfaceControl.Transaction.class); + // Single close + final TransitionInfo.Change open = createAppChange(openTaskId, TRANSIT_OPEN, + FLAG_BACK_GESTURE_ANIMATED | FLAG_MOVED_TO_TOP); + final TransitionInfo.Change close = createAppChange(closeTaskId, TRANSIT_CLOSE, + FLAG_BACK_GESTURE_ANIMATED); + + TransitionInfo tInfo = createTransitionInfo(TRANSIT_CLOSE, open, close); + mBackTransitionHandler.mCloseTransitionRequested = true; + Transitions.TransitionFinishCallback callback = + mock(Transitions.TransitionFinishCallback.class); + mBackTransitionHandler.startAnimation(mockBinder, tInfo, st, ft, callback); + verify(mBackTransitionHandler).handleCloseTransition( + eq(tInfo), eq(st), eq(ft), eq(callback)); + mBackTransitionHandler.onAnimationFinished(); + verify(callback).onTransitionFinished(any()); + mBackTransitionHandler.mCloseTransitionRequested = false; + + // PREPARE + CLOSE + tInfo = createTransitionInfo(TRANSIT_PREPARE_BACK_NAVIGATION, open); + callback = mock(Transitions.TransitionFinishCallback.class); + mBackTransitionHandler.startAnimation(mockBinder, tInfo, st, ft, callback); + verify(mBackTransitionHandler).handlePrepareTransition( + eq(tInfo), eq(st), eq(ft), eq(callback)); + mBackTransitionHandler.mCloseTransitionRequested = true; + TransitionInfo tInfo2 = createTransitionInfo(TRANSIT_CLOSE, close); + Transitions.TransitionFinishCallback mergeCallback = + mock(Transitions.TransitionFinishCallback.class); + mBackTransitionHandler.mergeAnimation( + mock(IBinder.class), tInfo2, st, mock(IBinder.class), mergeCallback); + mBackTransitionHandler.onAnimationFinished(); + verify(callback).onTransitionFinished(any()); + verify(mergeCallback).onTransitionFinished(any()); + mBackTransitionHandler.mCloseTransitionRequested = false; + + // PREPARE contains close info + tInfo = createTransitionInfo(TRANSIT_PREPARE_BACK_NAVIGATION, open, close); + callback = mock(Transitions.TransitionFinishCallback.class); + mBackTransitionHandler.mCloseTransitionRequested = true; + mBackTransitionHandler.startAnimation(mockBinder, tInfo, st, ft, callback); + verify(mBackTransitionHandler).handleCloseTransition( + eq(tInfo), eq(st), eq(ft), eq(callback)); + mBackTransitionHandler.onAnimationFinished(); + verify(callback).onTransitionFinished(any()); + mBackTransitionHandler.mCloseTransitionRequested = false; + + // PREPARE then Cancel + tInfo = createTransitionInfo(TRANSIT_PREPARE_BACK_NAVIGATION, open); + callback = mock(Transitions.TransitionFinishCallback.class); + final TransitionRequestInfo requestInfo = new TransitionRequestInfo( + TRANSIT_PREPARE_BACK_NAVIGATION, null /* triggerTask */, + null /* remoteTransition */); + mBackTransitionHandler.handleRequest(mockBinder, requestInfo); + mBackTransitionHandler.startAnimation(mockBinder, tInfo, st, ft, callback); + verify(mBackTransitionHandler).handlePrepareTransition( + eq(tInfo), eq(st), eq(ft), eq(callback)); + + mBackTransitionHandler.onAnimationFinished(); + final TransitionInfo.Change openToClose = createAppChange(openTaskId, TRANSIT_CLOSE, + FLAG_BACK_GESTURE_ANIMATED); + tInfo2 = createTransitionInfo(TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION, openToClose); + mBackTransitionHandler.mClosePrepareTransition = mock(IBinder.class); + mergeCallback = mock(Transitions.TransitionFinishCallback.class); + mBackTransitionHandler.mergeAnimation(mBackTransitionHandler.mClosePrepareTransition, + tInfo2, st, mock(IBinder.class), mergeCallback); + assertTrue("Change should be consumed", tInfo2.getChanges().isEmpty()); + verify(callback).onTransitionFinished(any()); + } + + @Test + public void testCancelUnexpectedTransition() { + final int openTaskId = 1; + final int closeTaskId = 2; + mController.mApps = createAppAnimationTargets(openTaskId, closeTaskId); + final IBinder mockBinder = mock(IBinder.class); + final SurfaceControl.Transaction st = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction ft = mock(SurfaceControl.Transaction.class); + final TransitionInfo.Change open = createAppChange(openTaskId, TRANSIT_OPEN, + FLAG_BACK_GESTURE_ANIMATED | FLAG_MOVED_TO_TOP); + final TransitionInfo.Change close = createAppChange(closeTaskId, TRANSIT_CLOSE, + FLAG_BACK_GESTURE_ANIMATED); + + // Didn't trigger close transition + mBackTransitionHandler.mCloseTransitionRequested = false; + TransitionInfo prepareInfo = createTransitionInfo(TRANSIT_PREPARE_BACK_NAVIGATION, + open, close); + final Transitions.TransitionFinishCallback callback = + mock(Transitions.TransitionFinishCallback.class); + mBackTransitionHandler.handleRequest(mockBinder, mock(TransitionRequestInfo.class)); + boolean canHandle = mBackTransitionHandler.startAnimation( + mockBinder, prepareInfo, st, ft, callback); + assertFalse("Should not handle transition", canHandle); + assertNull(mBackTransitionHandler.mOnAnimationFinishCallback); + + // Didn't trigger close transition, but receive close target. + final TransitionRequestInfo requestInfo = new TransitionRequestInfo( + TRANSIT_PREPARE_BACK_NAVIGATION, null /* triggerTask */, + null /* remoteTransition */); + prepareInfo = createTransitionInfo(TRANSIT_PREPARE_BACK_NAVIGATION, open); + final Transitions.TransitionFinishCallback callback2 = + mock(Transitions.TransitionFinishCallback.class); + mBackTransitionHandler.handleRequest(mockBinder, requestInfo); + canHandle = mBackTransitionHandler.startAnimation(mockBinder, + prepareInfo, st, ft, callback2); + assertTrue("Handle prepare transition" , canHandle); + verify(mBackTransitionHandler).handlePrepareTransition( + eq(prepareInfo), eq(st), eq(ft), eq(callback2)); + final TransitionInfo closeInfo = createTransitionInfo(TRANSIT_CLOSE, close); + Transitions.TransitionFinishCallback mergeCallback = + mock(Transitions.TransitionFinishCallback.class); + mBackTransitionHandler.mergeAnimation(mock(IBinder.class), closeInfo, ft, + mock(IBinder.class), mergeCallback); + verify(callback2).onTransitionFinished(any()); + verify(mergeCallback, never()).onTransitionFinished(any()); + + // Didn't trigger close transition, but contains open target. + final int openTaskId2 = 3; + final Transitions.TransitionFinishCallback callback3 = + mock(Transitions.TransitionFinishCallback.class); + mBackTransitionHandler.handleRequest(mockBinder, requestInfo); + canHandle = mBackTransitionHandler.startAnimation( + mockBinder, prepareInfo, st, ft, callback3); + assertTrue("Handle prepare transition" , canHandle); + verify(mBackTransitionHandler).handlePrepareTransition( + eq(prepareInfo), eq(st), eq(ft), eq(callback3)); + final TransitionInfo.Change open2 = createAppChange( + openTaskId2, TRANSIT_OPEN, FLAG_MOVED_TO_TOP); + final TransitionInfo openInfo = createTransitionInfo(TRANSIT_OPEN, open2, close); + mergeCallback = mock(Transitions.TransitionFinishCallback.class); + mBackTransitionHandler.mergeAnimation(mock(IBinder.class), openInfo, ft, + mock(IBinder.class), mergeCallback); + verify(callback3).onTransitionFinished(any()); + verify(mergeCallback, never()).onTransitionFinished(any()); + } + + private RemoteAnimationTarget[] createAppAnimationTargets(int openTaskId, int closeTaskId) { + final RemoteAnimationTarget openT = createSingleAnimationTarget(openTaskId, + RemoteAnimationTarget.MODE_OPENING); + final RemoteAnimationTarget closeT = createSingleAnimationTarget(closeTaskId, + RemoteAnimationTarget.MODE_CLOSING); + return new RemoteAnimationTarget[]{openT, closeT}; + } + + private RemoteAnimationTarget createSingleAnimationTarget(int taskId, int mode) { + final Rect fakeR = new Rect(); + final Point fakeP = new Point(); + final ActivityManager.RunningTaskInfo openTaskInfo = new ActivityManager.RunningTaskInfo(); + openTaskInfo.taskId = taskId; + openTaskInfo.token = new WindowContainerToken(mock(IWindowContainerToken.class)); + return new RemoteAnimationTarget( + taskId, mode, mock(SurfaceControl.class), false, fakeR, fakeR, + 0, fakeP, fakeR, fakeR, new WindowConfiguration(), false, + mock(SurfaceControl.class), fakeR, openTaskInfo, false); + } + private TransitionInfo.Change createAppChange( + int taskId, @TransitionInfo.TransitionMode int mode, + @TransitionInfo.ChangeFlags int flags) { + final TransitionInfo.Change change; + SurfaceControl.Builder b = new SurfaceControl.Builder() + .setName("test task"); + if (taskId != INVALID_TASK_ID) { + final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.taskId = taskId; + taskInfo.token = new WindowContainerToken(mock(IWindowContainerToken.class)); + change = new TransitionInfo.Change( + taskInfo.token, b.build()); + change.setTaskInfo(taskInfo); + } else { + change = new TransitionInfo.Change( + null, b.build()); + + } + change.setMode(mode); + change.setFlags(flags); + return change; + } + + private static TransitionInfo createTransitionInfo( + @WindowManager.TransitionType int type, TransitionInfo.Change ... changes) { + final TransitionInfo info = new TransitionInfo(type, 0); + for (int i = 0; i < changes.length; ++i) { + info.addChange(changes[i]); + } + return info; + } + private void verifySystemBackBehavior(int type, BackAnimationRunner animation) throws RemoteException { final BackAnimationRunner animationRunner = spy(animation); @@ -661,7 +887,7 @@ public class BackAnimationControllerTest extends ShellTestCase { RemoteAnimationTarget[] targets = new RemoteAnimationTarget[]{animationTarget}; if (mController.mBackAnimationAdapter != null) { mController.mBackAnimationAdapter.getRunner().onAnimationStart( - targets, null, null, mBackAnimationFinishedCallback); + targets, null /* prepareOpenTransition */, mBackAnimationFinishedCallback); mShellExecutor.flushAll(); } } @@ -677,7 +903,8 @@ public class BackAnimationControllerTest extends ShellTestCase { new BackAnimationRunner( mAnimatorCallback, mBackAnimationRunner, - mContext)); + mContext, + mHandler)); } private void unregisterAnimation(int type) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java index 4d0348b4f470..9b019ddb8362 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java @@ -177,6 +177,31 @@ public class BackProgressAnimatorTest { assertEquals(1, finishCallbackCalled.getCount()); } + @Test + public void testOnBackInvokedFinishCallbackNotInvokedWhenRemoved() throws InterruptedException { + // Give the animator some progress. + final BackMotionEvent backEvent = backMotionEventFrom(100, mTargetProgress); + mMainThreadHandler.post( + () -> mProgressAnimator.onBackProgressed(backEvent)); + mTargetProgressCalled.await(1, TimeUnit.SECONDS); + assertNotNull(mReceivedBackEvent); + + // Trigger back invoked animation + CountDownLatch finishCallbackCalled = new CountDownLatch(1); + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> mProgressAnimator.onBackInvoked(finishCallbackCalled::countDown)); + + // remove onBackCancelled finishCallback (while progress is still animating to 0) + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> mProgressAnimator.removeOnBackInvokedFinishCallback()); + + // call reset (which triggers the finishCallback invocation, if one is present) + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> mProgressAnimator.reset()); + + // verify that finishCallback is not invoked + assertEquals(1, finishCallbackCalled.getCount()); + } + private void onGestureProgress(BackEvent backEvent) { if (mTargetProgress == backEvent.getProgress()) { mReceivedBackEvent = backEvent; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt index 080ad901c656..5b5ef6f48789 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt @@ -22,6 +22,7 @@ import android.app.WindowConfiguration import android.graphics.Color import android.graphics.Point import android.graphics.Rect +import android.os.Handler import android.os.RemoteException import android.testing.AndroidTestingRunner import android.testing.TestableLooper @@ -66,6 +67,7 @@ class CustomCrossActivityBackAnimationTest : ShellTestCase() { @Mock private lateinit var transitionAnimation: TransitionAnimation @Mock private lateinit var appCompatTaskInfo: AppCompatTaskInfo @Mock private lateinit var transaction: Transaction + @Mock private lateinit var handler: Handler private lateinit var customCrossActivityBackAnimation: CustomCrossActivityBackAnimation private lateinit var customAnimationLoader: CustomAnimationLoader @@ -80,7 +82,8 @@ class CustomCrossActivityBackAnimationTest : ShellTestCase() { backAnimationBackground, rootTaskDisplayAreaOrganizer, transaction, - customAnimationLoader + customAnimationLoader, + handler, ) whenever(transitionAnimation.loadAppTransitionAnimation(eq(PACKAGE_NAME), eq(OPEN_RES_ID))) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataRepositoryTest.kt index e35995775f76..9ec62c965a14 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataRepositoryTest.kt @@ -124,6 +124,7 @@ class BubbleDataRepositoryTest : ShellTestCase() { private val testHandler = Handler(Looper.getMainLooper()) private val mainExecutor = HandlerExecutor(testHandler) + private val bgExecutor = HandlerExecutor(testHandler) private val launcherApps = mock<LauncherApps>() private val persistedBubbles = SparseArray<List<BubbleEntity>>() @@ -134,7 +135,8 @@ class BubbleDataRepositoryTest : ShellTestCase() { @Before fun setup() { persistentRepository = BubblePersistentRepository(mContext) - dataRepository = spy(BubbleDataRepository(launcherApps, mainExecutor, persistentRepository)) + dataRepository = + spy(BubbleDataRepository(launcherApps, mainExecutor, bgExecutor, persistentRepository)) persistedBubbles.put(0, user0BubbleEntities) persistedBubbles.put(1, user1BubbleEntities) 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 93e405131a58..6fa37885b724 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 @@ -27,6 +27,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @@ -49,8 +50,8 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.bubbles.BubbleData.TimeSource; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; -import com.android.wm.shell.common.bubbles.BubbleBarUpdate; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; import com.google.common.collect.ImmutableList; @@ -116,6 +117,8 @@ public class BubbleDataTest extends ShellTestCase { private BubbleEducationController mEducationController; @Mock private ShellExecutor mMainExecutor; + @Mock + private ShellExecutor mBgExecutor; @Captor private ArgumentCaptor<BubbleData.Update> mUpdateCaptor; @@ -143,47 +146,47 @@ public class BubbleDataTest extends ShellTestCase { when(ranking.isTextChanged()).thenReturn(true); mEntryInterruptive = createBubbleEntry(1, "interruptive", "package.d", ranking); mBubbleInterruptive = new Bubble(mEntryInterruptive, mBubbleMetadataFlagListener, null, - mMainExecutor); + mMainExecutor, mBgExecutor); mEntryDismissed = createBubbleEntry(1, "dismissed", "package.d", null); mBubbleDismissed = new Bubble(mEntryDismissed, mBubbleMetadataFlagListener, null, - mMainExecutor); + mMainExecutor, mBgExecutor); mEntryLocusId = createBubbleEntry(1, "keyLocus", "package.e", null, new LocusId("locusId1")); mBubbleLocusId = new Bubble(mEntryLocusId, mBubbleMetadataFlagListener, null /* pendingIntentCanceledListener */, - mMainExecutor); + mMainExecutor, mBgExecutor); mBubbleA1 = new Bubble(mEntryA1, mBubbleMetadataFlagListener, mPendingIntentCanceledListener, - mMainExecutor); + mMainExecutor, mBgExecutor); mBubbleA2 = new Bubble(mEntryA2, mBubbleMetadataFlagListener, mPendingIntentCanceledListener, - mMainExecutor); + mMainExecutor, mBgExecutor); mBubbleA3 = new Bubble(mEntryA3, mBubbleMetadataFlagListener, mPendingIntentCanceledListener, - mMainExecutor); + mMainExecutor, mBgExecutor); mBubbleB1 = new Bubble(mEntryB1, mBubbleMetadataFlagListener, mPendingIntentCanceledListener, - mMainExecutor); + mMainExecutor, mBgExecutor); mBubbleB2 = new Bubble(mEntryB2, mBubbleMetadataFlagListener, mPendingIntentCanceledListener, - mMainExecutor); + mMainExecutor, mBgExecutor); mBubbleB3 = new Bubble(mEntryB3, mBubbleMetadataFlagListener, mPendingIntentCanceledListener, - mMainExecutor); + mMainExecutor, mBgExecutor); mBubbleC1 = new Bubble(mEntryC1, mBubbleMetadataFlagListener, mPendingIntentCanceledListener, - mMainExecutor); + mMainExecutor, mBgExecutor); Intent appBubbleIntent = new Intent(mContext, BubblesTestActivity.class); appBubbleIntent.setPackage(mContext.getPackageName()); @@ -191,12 +194,12 @@ public class BubbleDataTest extends ShellTestCase { appBubbleIntent, new UserHandle(1), mock(Icon.class), - mMainExecutor); + mMainExecutor, mBgExecutor); mPositioner = new TestableBubblePositioner(mContext, mContext.getSystemService(WindowManager.class)); mBubbleData = new BubbleData(getContext(), mBubbleLogger, mPositioner, mEducationController, - mMainExecutor); + mMainExecutor, mBgExecutor); // Used by BubbleData to set lastAccessedTime when(mTimeSource.currentTimeMillis()).thenReturn(1000L); @@ -255,6 +258,45 @@ public class BubbleDataTest extends ShellTestCase { } @Test + public void testRemoveBubbleInLauncher_beforeBubbleUpdate_processedAfter_shouldNotBeRemoved() { + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + mBubbleData.setListener(mListener); + + sendUpdatedEntryAtTime(mEntryA2, 3000); + + verifyUpdateReceived(); + assertThat(mBubbleData.hasBubbleInStackWithKey(mEntryA2.getKey())).isTrue(); + assertThat(mBubbleData.getBubbleInStackWithKey(mEntryA2.getKey()).getLastActivity()) + .isEqualTo(3000); + + // dismiss the bubble with a timestamp in the past + mBubbleData.dismissBubbleWithKey( + mEntryA2.getKey(), Bubbles.DISMISS_USER_GESTURE_FROM_LAUNCHER, 2500); + + verifyNoMoreInteractions(mListener); + assertThat(mBubbleData.hasBubbleInStackWithKey(mEntryA2.getKey())).isTrue(); + } + + @Test + public void testRemoveBubbleInLauncher_isNotSentBackToLauncher() { + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + mBubbleData.setListener(mListener); + + mBubbleData.dismissBubbleWithKey( + mEntryA2.getKey(), Bubbles.DISMISS_USER_GESTURE_FROM_LAUNCHER, 4000); + verifyUpdateReceived(); + + BubbleData.Update update = mUpdateCaptor.getValue(); + assertThat(update.removedBubbles).hasSize(1); + assertThat(update.removedBubbles.getFirst().first.getKey()).isEqualTo(mBubbleA2.getKey()); + + BubbleBarUpdate bubbleBarUpdate = update.toBubbleBarUpdate(); + assertThat(bubbleBarUpdate.removedBubbles).isEmpty(); + } + + @Test public void ifSuppress_hideFlyout() { // Setup mBubbleData.setListener(mListener); @@ -1415,15 +1457,13 @@ public class BubbleDataTest extends ShellTestCase { sendUpdatedEntryAtTime(entry, postTime, true /* isTextChanged */); } - private void sendUpdatedEntryAtTime(BubbleEntry entry, long postTime, - boolean textChanged) { + private void sendUpdatedEntryAtTime(BubbleEntry entry, long postTime, boolean textChanged) { setPostTime(entry, postTime); // BubbleController calls this: Bubble b = mBubbleData.getOrCreateBubble(entry, null /* persistedBubble */); b.setTextChangedForTest(textChanged); // And then this - mBubbleData.notificationEntryUpdated(b, false /* suppressFlyout*/, - true /* showInShade */); + mBubbleData.notificationEntryUpdated(b, false /* suppressFlyout*/, true /* showInShade */); } private void changeExpandedStateAtTime(boolean shouldBeExpanded, long time) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java index afec1ee12341..dca5fc4c2fe0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java @@ -43,7 +43,7 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.bubbles.BubbleInfo; +import com.android.wm.shell.shared.bubbles.BubbleInfo; import org.junit.Before; import org.junit.Test; @@ -61,6 +61,8 @@ public class BubbleTest extends ShellTestCase { private StatusBarNotification mSbn; @Mock private ShellExecutor mMainExecutor; + @Mock + private ShellExecutor mBgExecutor; private BubbleEntry mBubbleEntry; private Bundle mExtras; @@ -85,7 +87,8 @@ public class BubbleTest extends ShellTestCase { when(mNotif.getBubbleMetadata()).thenReturn(metadata); when(mSbn.getKey()).thenReturn("mock"); mBubbleEntry = new BubbleEntry(mSbn, null, true, false, false, false); - mBubble = new Bubble(mBubbleEntry, mBubbleMetadataFlagListener, null, mMainExecutor); + mBubble = new Bubble(mBubbleEntry, mBubbleMetadataFlagListener, null, mMainExecutor, + mBgExecutor); } @Test @@ -176,7 +179,8 @@ public class BubbleTest extends ShellTestCase { @Test public void testBubbleIsConversation_hasNoShortcut() { - Bubble bubble = new Bubble(mBubbleEntry, mBubbleMetadataFlagListener, null, mMainExecutor); + Bubble bubble = new Bubble(mBubbleEntry, mBubbleMetadataFlagListener, null, mMainExecutor, + mBgExecutor); assertThat(bubble.getShortcutInfo()).isNull(); assertThat(bubble.isConversation()).isFalse(); } @@ -199,7 +203,7 @@ public class BubbleTest extends ShellTestCase { Intent intent = new Intent(mContext, BubblesTestActivity.class); intent.setPackage(mContext.getPackageName()); Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1 /* userId */), - null /* icon */, mMainExecutor); + null /* icon */, mMainExecutor, mBgExecutor); BubbleInfo bubbleInfo = bubble.asBubbleBarBubble(); assertThat(bubble.getShortcutInfo()).isNull(); @@ -215,6 +219,6 @@ public class BubbleTest extends ShellTestCase { .build(); return new Bubble("mockKey", shortcutInfo, 10, Resources.ID_NULL, "mockTitle", 0 /* taskId */, "mockLocus", true /* isDismissible */, - mMainExecutor, mBubbleMetadataFlagListener); + mMainExecutor, mBgExecutor, mBubbleMetadataFlagListener); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt index 4a4c5e860bb2..4ac066e4ffb0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt @@ -70,10 +70,10 @@ class BubbleViewInfoTest : ShellTestCase() { private lateinit var bubble: Bubble private lateinit var bubbleController: BubbleController private lateinit var mainExecutor: ShellExecutor + private lateinit var bgExecutor: ShellExecutor private lateinit var bubbleStackView: BubbleStackView private lateinit var bubbleBarLayerView: BubbleBarLayerView private lateinit var bubblePositioner: BubblePositioner - private lateinit var expandedViewManager: BubbleExpandedViewManager private val bubbleTaskViewFactory = BubbleTaskViewFactory { BubbleTaskView(mock<TaskView>(), mock<Executor>()) @@ -92,6 +92,7 @@ class BubbleViewInfoTest : ShellTestCase() { ) mainExecutor = TestShellExecutor() + bgExecutor = TestShellExecutor() val windowManager = context.getSystemService(WindowManager::class.java) val shellInit = ShellInit(mainExecutor) val shellCommandHandler = ShellCommandHandler() @@ -104,7 +105,8 @@ class BubbleViewInfoTest : ShellTestCase() { mock<BubbleLogger>(), bubblePositioner, BubbleEducationController(context), - mainExecutor + mainExecutor, + bgExecutor ) val surfaceSynchronizer = { obj: Runnable -> obj.run() } @@ -132,7 +134,7 @@ class BubbleViewInfoTest : ShellTestCase() { null, mainExecutor, mock<Handler>(), - mock<ShellExecutor>(), + bgExecutor, mock<TaskViewTransitions>(), mock<Transitions>(), mock<SyncTransactionQueue>(), @@ -152,7 +154,6 @@ class BubbleViewInfoTest : ShellTestCase() { bubbleController, mainExecutor ) - expandedViewManager = BubbleExpandedViewManager.fromBubbleController(bubbleController) bubbleBarLayerView = BubbleBarLayerView(context, bubbleController, bubbleData) } @@ -162,7 +163,6 @@ class BubbleViewInfoTest : ShellTestCase() { val info = BubbleViewInfoTask.BubbleViewInfo.populate( context, - expandedViewManager, bubbleTaskViewFactory, bubblePositioner, bubbleStackView, @@ -190,9 +190,7 @@ class BubbleViewInfoTest : ShellTestCase() { val info = BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar( context, - expandedViewManager, bubbleTaskViewFactory, - bubblePositioner, bubbleBarLayerView, iconFactory, bubble, @@ -226,9 +224,7 @@ class BubbleViewInfoTest : ShellTestCase() { val info = BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar( context, - expandedViewManager, bubbleTaskViewFactory, - bubblePositioner, bubbleBarLayerView, iconFactory, bubble, @@ -256,7 +252,7 @@ class BubbleViewInfoTest : ShellTestCase() { "mockLocus", true /* isDismissible */, mainExecutor, - metadataFlagListener - ) + bgExecutor, + metadataFlagListener) } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/OWNERS new file mode 100644 index 000000000000..983e8784a2ce --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 555586 +# includes OWNERS from parent directories
\ No newline at end of file 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 2c0aa12f22d2..f8f0db930e6c 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 @@ -43,6 +43,7 @@ import android.view.inputmethod.ImeTracker; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; @@ -83,7 +84,7 @@ public class DisplayImeControllerTest extends ShellTestCase { } }, mExecutor) { @Override - void removeImeSurface() { } + void removeImeSurface(int displayId) { } }.new PerDisplay(DEFAULT_DISPLAY, ROTATION_0); } @@ -119,7 +120,7 @@ public class DisplayImeControllerTest extends ShellTestCase { @Test public void reappliesVisibilityToChangedLeash() { verifyZeroInteractions(mT); - mPerDisplay.mImeShowing = true; + mPerDisplay.mImeShowing = false; mPerDisplay.insetsControlChanged(insetsStateWithIme(false), insetsSourceControl()); 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 index 669e433ba386..9df9956fa0e1 100644 --- 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 @@ -18,6 +18,7 @@ package com.android.wm.shell.common; import static android.view.Display.DEFAULT_DISPLAY; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -160,6 +161,19 @@ public class DisplayInsetsControllerTest extends ShellTestCase { assertTrue(secondListener.hideInsetsCount == 1); } + @Test + public void testGlobalListenerCallback() throws RemoteException { + TrackedListener globalListener = new TrackedListener(); + addDisplay(SECOND_DISPLAY); + mController.addGlobalInsetsChangedListener(globalListener); + + mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsChanged(null); + mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsChanged(null); + mExecutor.flushAll(); + + assertEquals(2, globalListener.insetsChangedCount); + } + private void addDisplay(int displayId) throws RemoteException { mController.onDisplayAdded(displayId); verify(mWm, times(mInsetsControllersByDisplayId.size() + 1)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/ImeListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/ImeListenerTest.kt new file mode 100644 index 000000000000..3b0a0722968b --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/ImeListenerTest.kt @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2024 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.content.res.Configuration +import android.content.res.Resources +import android.graphics.Insets +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import android.view.DisplayCutout +import android.view.DisplayInfo +import android.view.InsetsSource.ID_IME +import android.view.InsetsState +import android.view.Surface +import android.view.WindowInsets.Type +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.wm.shell.ShellTestCase +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.kotlin.whenever + + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class ImeListenerTest : ShellTestCase() { + private lateinit var imeListener: CachingImeListener + private lateinit var displayLayout: DisplayLayout + + @Mock private lateinit var displayController: DisplayController + @Before + fun setUp() { + val resources = createResources(40, 50, false) + val displayInfo = createDisplayInfo(1000, 1500, 0, Surface.ROTATION_0) + displayLayout = DisplayLayout(displayInfo, resources, false, false) + whenever(displayController.getDisplayLayout(DEFAULT_DISPLAY_ID)).thenReturn(displayLayout) + imeListener = CachingImeListener(displayController, DEFAULT_DISPLAY_ID) + } + + @Test + fun testImeAppears() { + val insetsState = createInsetsStateWithIme(true, DEFAULT_IME_HEIGHT) + imeListener.insetsChanged(insetsState) + assertTrue("Ime insets source should become visible", imeListener.cachedImeVisible) + assertEquals(DEFAULT_IME_HEIGHT, imeListener.cachedImeHeight) + } + + @Test + fun testImeAppears_thenDisappears() { + // Send insetsState with an IME as a visible source. + val insetsStateWithIme = createInsetsStateWithIme(true, DEFAULT_IME_HEIGHT) + imeListener.insetsChanged(insetsStateWithIme) + + // Send insetsState without IME. + val insetsStateWithoutIme = createInsetsStateWithIme(false, 0) + imeListener.insetsChanged(insetsStateWithoutIme) + + assertFalse("Ime insets source should become invisible", + imeListener.cachedImeVisible) + assertEquals(0, imeListener.cachedImeHeight) + } + + private fun createInsetsStateWithIme(isVisible: Boolean, imeHeight: Int): InsetsState { + val stableBounds = Rect() + displayLayout.getStableBounds(stableBounds) + val insetsState = InsetsState() + + val insetsSource = insetsState.getOrCreateSource(ID_IME, Type.ime()) + insetsSource.setVisible(isVisible) + insetsSource.setFrame(stableBounds.left, stableBounds.bottom - imeHeight, + stableBounds.right, stableBounds.bottom) + return insetsState + } + + private fun createDisplayInfo(width: Int, height: Int, cutoutHeight: Int, + rotation: Int): DisplayInfo { + val info = DisplayInfo() + info.logicalWidth = width + info.logicalHeight = height + info.rotation = rotation + if (cutoutHeight > 0) { + info.displayCutout = DisplayCutout( + Insets.of(0, cutoutHeight, 0, 0) /* safeInsets */, + null /* boundLeft */, + Rect(width / 2 - cutoutHeight, 0, width / 2 + cutoutHeight, + cutoutHeight) /* boundTop */, null /* boundRight */, + null /* boundBottom */) + } else { + info.displayCutout = DisplayCutout.NO_CUTOUT + } + info.logicalDensityDpi = 300 + return info + } + + private fun createResources(navLand: Int, navPort: Int, navMoves: Boolean): Resources { + val cfg = Configuration() + cfg.uiMode = Configuration.UI_MODE_TYPE_NORMAL + val res = Mockito.mock(Resources::class.java) + Mockito.doReturn(navLand).whenever(res).getDimensionPixelSize( + R.dimen.navigation_bar_height_landscape_car_mode) + Mockito.doReturn(navPort).whenever(res).getDimensionPixelSize( + R.dimen.navigation_bar_height_car_mode) + Mockito.doReturn(navLand).whenever(res).getDimensionPixelSize( + R.dimen.navigation_bar_width_car_mode) + Mockito.doReturn(navLand).whenever(res).getDimensionPixelSize( + R.dimen.navigation_bar_height_landscape) + Mockito.doReturn(navPort).whenever(res).getDimensionPixelSize( + R.dimen.navigation_bar_height) + Mockito.doReturn(navLand).whenever(res).getDimensionPixelSize( + R.dimen.navigation_bar_width) + Mockito.doReturn(navMoves).whenever(res).getBoolean(R.bool.config_navBarCanMove) + Mockito.doReturn(cfg).whenever(res).configuration + return res + } + + private class CachingImeListener( + displayController: DisplayController, + displayId: Int + ) : ImeListener(displayController, displayId) { + var cachedImeVisible = false + var cachedImeHeight = 0 + public override fun onImeVisibilityChanged(imeVisible: Boolean, imeHeight: Int) { + cachedImeVisible = imeVisible + cachedImeHeight = imeHeight + } + } + + companion object { + private const val DEFAULT_DISPLAY_ID = 0 + private const val DEFAULT_IME_HEIGHT = 500 + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java index 636c6326d213..cf69704a0470 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java @@ -25,6 +25,7 @@ import static org.mockito.Mockito.verify; import android.content.res.Configuration; import android.graphics.Rect; +import android.os.Handler; import android.os.SystemClock; import android.provider.DeviceConfig; import android.view.InputDevice; @@ -55,6 +56,7 @@ public class DividerViewTest extends ShellTestCase { private @Mock DisplayController mDisplayController; private @Mock DisplayImeController mDisplayImeController; private @Mock ShellTaskOrganizer mTaskOrganizer; + private @Mock Handler mHandler; private SplitLayout mSplitLayout; private DividerView mDividerView; @@ -65,12 +67,12 @@ public class DividerViewTest extends ShellTestCase { Configuration configuration = getConfiguration(); mSplitLayout = new SplitLayout("TestSplitLayout", mContext, configuration, mSplitLayoutHandler, mCallbacks, mDisplayController, mDisplayImeController, - mTaskOrganizer, SplitLayout.PARALLAX_NONE); + mTaskOrganizer, SplitLayout.PARALLAX_NONE, mHandler); SplitWindowManager splitWindowManager = new SplitWindowManager("TestSplitWindowManager", mContext, configuration, mCallbacks); splitWindowManager.init(mSplitLayout, new InsetsState(), false /* isRestoring */); - mDividerView = spy((DividerView) splitWindowManager.getDividerView()); + mDividerView = spy(splitWindowManager.getDividerView()); } @Test 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 cfe8e07aa6e5..177e47a342f6 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 @@ -19,9 +19,7 @@ package com.android.wm.shell.common.split; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_START_AND_DISMISS; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50; import static com.google.common.truth.Truth.assertThat; @@ -37,6 +35,7 @@ import static org.mockito.Mockito.verify; import android.app.ActivityManager; import android.content.res.Configuration; import android.graphics.Rect; +import android.os.Handler; import android.window.WindowContainerTransaction; import androidx.test.annotation.UiThreadTest; @@ -67,6 +66,7 @@ public class SplitLayoutTests extends ShellTestCase { @Mock DisplayImeController mDisplayImeController; @Mock ShellTaskOrganizer mTaskOrganizer; @Mock WindowContainerTransaction mWct; + @Mock Handler mHandler; @Captor ArgumentCaptor<Runnable> mRunnableCaptor; private SplitLayout mSplitLayout; @@ -82,7 +82,8 @@ public class SplitLayoutTests extends ShellTestCase { mDisplayController, mDisplayImeController, mTaskOrganizer, - SplitLayout.PARALLAX_NONE)); + SplitLayout.PARALLAX_NONE, + mHandler)); } @Test @@ -150,8 +151,8 @@ public class SplitLayoutTests extends ShellTestCase { @UiThreadTest public void testSnapToDismissStart() { // verify it callbacks properly when the snap target indicates dismissing split. - DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */, - SNAP_TO_START_AND_DISMISS); + DividerSnapAlgorithm.SnapTarget snapTarget = + mSplitLayout.mDividerSnapAlgorithm.getDismissStartTarget(); mSplitLayout.snapToTarget(mSplitLayout.getDividerPosition(), snapTarget); waitDividerFlingFinished(); @@ -162,8 +163,8 @@ public class SplitLayoutTests extends ShellTestCase { @UiThreadTest public void testSnapToDismissEnd() { // verify it callbacks properly when the snap target indicates dismissing split. - DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */, - SNAP_TO_END_AND_DISMISS); + DividerSnapAlgorithm.SnapTarget snapTarget = + mSplitLayout.mDividerSnapAlgorithm.getDismissEndTarget(); mSplitLayout.snapToTarget(mSplitLayout.getDividerPosition(), snapTarget); waitDividerFlingFinished(); @@ -203,9 +204,4 @@ public class SplitLayoutTests extends ShellTestCase { new Rect(0, 0, 1080, 2160)); return configuration; } - - private static DividerSnapAlgorithm.SnapTarget getSnapTarget(int position, int flag) { - return new DividerSnapAlgorithm.SnapTarget( - position /* position */, position /* taskPosition */, flag); - } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt index 4cd2a366f5eb..ecaf970ae389 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt @@ -16,8 +16,10 @@ package com.android.wm.shell.compatui +import android.content.ComponentName import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest +import com.android.internal.R import com.android.wm.shell.ShellTestCase import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask import org.junit.Assert.assertFalse @@ -34,26 +36,55 @@ import org.junit.runner.RunWith @RunWith(AndroidTestingRunner::class) @SmallTest class AppCompatUtilsTest : ShellTestCase() { - @Test - fun testIsSingleTopActivityTranslucent() { - assertTrue(isSingleTopActivityTranslucent( + fun testIsTopActivityExemptFromDesktopWindowing_topActivityTransparent() { + assertTrue(isTopActivityExemptFromDesktopWindowing(mContext, createFreeformTask(/* displayId */ 0) .apply { isTopActivityTransparent = true numActivities = 1 })) - assertFalse(isSingleTopActivityTranslucent( + assertFalse(isTopActivityExemptFromDesktopWindowing(mContext, createFreeformTask(/* displayId */ 0) .apply { isTopActivityTransparent = true numActivities = 0 })) - assertFalse(isSingleTopActivityTranslucent( + } + + @Test + fun testIsTopActivityExemptFromDesktopWindowing_singleTopActivity() { + assertTrue(isTopActivityExemptFromDesktopWindowing(mContext, + createFreeformTask(/* displayId */ 0) + .apply { + isTopActivityTransparent = true + numActivities = 1 + })) + assertFalse(isTopActivityExemptFromDesktopWindowing(mContext, createFreeformTask(/* displayId */ 0) .apply { isTopActivityTransparent = false numActivities = 1 })) } -}
\ No newline at end of file + + @Test + fun testIsTopActivityExemptFromDesktopWindowing__topActivityStyleFloating() { + assertFalse(isTopActivityExemptFromDesktopWindowing(mContext, + createFreeformTask(/* displayId */ 0) + .apply { + isTopActivityStyleFloating = true + })) + } + + @Test + fun testIsTopActivityExemptFromDesktopWindowing_systemUiTask() { + val systemUIPackageName = context.resources.getString(R.string.config_systemUi) + val baseComponent = ComponentName(systemUIPackageName, /* class */ "") + assertTrue(isTopActivityExemptFromDesktopWindowing(mContext, + createFreeformTask(/* displayId */ 0) + .apply { + baseActivity = baseComponent + })) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java index 9c008647104a..d5287e742c2c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java @@ -16,8 +16,6 @@ package com.android.wm.shell.compatui; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; import static android.view.WindowInsets.Type.navigationBars; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; @@ -34,17 +32,24 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.app.ActivityManager.RunningTaskInfo; -import android.app.CameraCompatTaskInfo.CameraCompatControlState; import android.app.TaskInfo; import android.content.Context; import android.content.res.Configuration; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.view.InsetsSource; import android.view.InsetsState; import android.view.accessibility.AccessibilityManager; +import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; +import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayController; @@ -55,6 +60,7 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.compatui.api.CompatUIInfo; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; @@ -63,6 +69,7 @@ import dagger.Lazy; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -82,6 +89,13 @@ public class CompatUIControllerTest extends ShellTestCase { private static final int DISPLAY_ID = 0; private static final int TASK_ID = 12; + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private CompatUIController mController; private ShellInit mShellInit; @Mock @@ -114,13 +128,17 @@ public class CompatUIControllerTest extends ShellTestCase { private CompatUIConfiguration mCompatUIConfiguration; @Mock private CompatUIShellCommandHandler mCompatUIShellCommandHandler; - @Mock private AccessibilityManager mAccessibilityManager; @Captor ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor; + @NonNull + private CompatUIStatusManager mCompatUIStatusManager; + + private boolean mInDesktopModePredicateResult; + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -140,11 +158,13 @@ public class CompatUIControllerTest extends ShellTestCase { doReturn(true).when(mMockRestartDialogLayout).createLayout(anyBoolean()); doReturn(true).when(mMockRestartDialogLayout).updateCompatInfo(any(), any(), anyBoolean()); + mCompatUIStatusManager = new CompatUIStatusManager(); mShellInit = spy(new ShellInit(mMockExecutor)); mController = new CompatUIController(mContext, mShellInit, mMockShellController, mMockDisplayController, mMockDisplayInsetsController, mMockImeController, mMockSyncQueue, mMockExecutor, mMockTransitionsLazy, mDockStateReader, - mCompatUIConfiguration, mCompatUIShellCommandHandler, mAccessibilityManager) { + mCompatUIConfiguration, mCompatUIShellCommandHandler, mAccessibilityManager, + mCompatUIStatusManager, i -> mInDesktopModePredicateResult) { @Override CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { @@ -168,28 +188,31 @@ public class CompatUIControllerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void instantiateController_addInitCallback() { verify(mShellInit, times(1)).addInitCallback(any(), any()); } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void instantiateController_registerKeyguardChangeListener() { verify(mMockShellController, times(1)).addKeyguardChangeListener(any()); } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testListenerRegistered() { verify(mMockDisplayController).addDisplayWindowListener(mController); verify(mMockImeController).addPositionProcessor(mController); } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnCompatInfoChanged() { - TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - CAMERA_COMPAT_CONTROL_HIDDEN); + TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); // Verify that the compat controls are added with non-null task listener. - mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo), @@ -200,9 +223,8 @@ public class CompatUIControllerTest extends ShellTestCase { // Verify that the compat controls and letterbox education are updated with new size compat // info. clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController); - taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); - mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ true); @@ -213,9 +235,9 @@ public class CompatUIControllerTest extends ShellTestCase { // Verify that compat controls and letterbox education are removed with null task listener. clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController); - mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, - /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), - /* taskListener= */ null); + mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID, + /* hasSizeCompat= */ true), + /* taskListener= */ null)); verify(mMockCompatLayout).release(); verify(mMockLetterboxEduLayout).release(); @@ -223,14 +245,14 @@ public class CompatUIControllerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnCompatInfoChanged_createLayoutReturnsFalse() { doReturn(false).when(mMockCompatLayout).createLayout(anyBoolean()); doReturn(false).when(mMockLetterboxEduLayout).createLayout(anyBoolean()); doReturn(false).when(mMockRestartDialogLayout).createLayout(anyBoolean()); - TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - CAMERA_COMPAT_CONTROL_HIDDEN); - mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo), @@ -240,7 +262,7 @@ public class CompatUIControllerTest extends ShellTestCase { // Verify that the layout is created again. clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mController); - mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); verify(mMockCompatLayout, never()).updateCompatInfo(any(), any(), anyBoolean()); verify(mMockLetterboxEduLayout, never()).updateCompatInfo(any(), any(), anyBoolean()); @@ -253,14 +275,14 @@ public class CompatUIControllerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnCompatInfoChanged_updateCompatInfoReturnsFalse() { doReturn(false).when(mMockCompatLayout).updateCompatInfo(any(), any(), anyBoolean()); doReturn(false).when(mMockLetterboxEduLayout).updateCompatInfo(any(), any(), anyBoolean()); doReturn(false).when(mMockRestartDialogLayout).updateCompatInfo(any(), any(), anyBoolean()); - TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - CAMERA_COMPAT_CONTROL_HIDDEN); - mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); verify(mController).createLetterboxEduWindowManager(any(), eq(taskInfo), @@ -270,7 +292,7 @@ public class CompatUIControllerTest extends ShellTestCase { clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout, mController); - mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ true); @@ -282,7 +304,7 @@ public class CompatUIControllerTest extends ShellTestCase { // Verify that the layout is created again. clearInvocations(mMockCompatLayout, mMockLetterboxEduLayout, mMockRestartDialogLayout, mController); - mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); verify(mMockCompatLayout, never()).updateCompatInfo(any(), any(), anyBoolean()); verify(mMockLetterboxEduLayout, never()).updateCompatInfo(any(), any(), anyBoolean()); @@ -296,6 +318,7 @@ public class CompatUIControllerTest extends ShellTestCase { @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnDisplayAdded() { mController.onDisplayAdded(DISPLAY_ID); mController.onDisplayAdded(DISPLAY_ID + 1); @@ -305,11 +328,11 @@ public class CompatUIControllerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnDisplayRemoved() { mController.onDisplayAdded(DISPLAY_ID); - mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, - /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), - mMockTaskListener); + mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID, + /* hasSizeCompat= */ true), mMockTaskListener)); mController.onDisplayRemoved(DISPLAY_ID + 1); @@ -328,9 +351,10 @@ public class CompatUIControllerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnDisplayConfigurationChanged() { - mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, - /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); + mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID, + /* hasSizeCompat= */ true), mMockTaskListener)); mController.onDisplayConfigurationChanged(DISPLAY_ID + 1, new Configuration()); @@ -346,10 +370,11 @@ public class CompatUIControllerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testInsetsChanged() { mController.onDisplayAdded(DISPLAY_ID); - mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, - /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); + mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID, + /* hasSizeCompat= */ true), mMockTaskListener)); InsetsState insetsState = new InsetsState(); InsetsSource insetsSource = new InsetsSource( InsetsSource.createId(null, 0, navigationBars()), navigationBars()); @@ -373,9 +398,10 @@ public class CompatUIControllerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testChangeLayoutsVisibilityOnImeShowHide() { - mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, - /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); + mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID, + /* hasSizeCompat= */ true), mMockTaskListener)); // Verify that the restart button is hidden after IME is showing. mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ true); @@ -385,9 +411,8 @@ public class CompatUIControllerTest extends ShellTestCase { verify(mMockRestartDialogLayout).updateVisibility(false); // Verify button remains hidden while IME is showing. - TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - CAMERA_COMPAT_CONTROL_HIDDEN); - mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ false); @@ -405,9 +430,10 @@ public class CompatUIControllerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testChangeLayoutsVisibilityOnKeyguardShowingChanged() { - mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, - /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); + mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID, + /* hasSizeCompat= */ true), mMockTaskListener)); // Verify that the restart button is hidden after keyguard becomes showing. mController.onKeyguardVisibilityChanged(true, false, false); @@ -417,9 +443,8 @@ public class CompatUIControllerTest extends ShellTestCase { verify(mMockRestartDialogLayout).updateVisibility(false); // Verify button remains hidden while keyguard is showing. - TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - CAMERA_COMPAT_CONTROL_HIDDEN); - mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ false); @@ -437,9 +462,10 @@ public class CompatUIControllerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testLayoutsRemainHiddenOnKeyguardShowingFalseWhenImeIsShowing() { - mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, - /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); + mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID, + /* hasSizeCompat= */ true), mMockTaskListener)); mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ true); mController.onKeyguardVisibilityChanged(true, false, false); @@ -466,9 +492,10 @@ public class CompatUIControllerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testLayoutsRemainHiddenOnImeHideWhenKeyguardIsShowing() { - mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, - /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); + mController.onCompatInfoChanged(new CompatUIInfo(createTaskInfo(DISPLAY_ID, TASK_ID, + /* hasSizeCompat= */ true), mMockTaskListener)); mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ true); mController.onKeyguardVisibilityChanged(true, false, false); @@ -495,45 +522,45 @@ public class CompatUIControllerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testRestartLayoutRecreatedIfNeeded() { - final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, - /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN); + final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); doReturn(true).when(mMockRestartDialogLayout) .needsToBeRecreated(any(TaskInfo.class), any(ShellTaskOrganizer.TaskListener.class)); - mController.onCompatInfoChanged(taskInfo, mMockTaskListener); - mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); verify(mMockRestartDialogLayout, times(2)) .createLayout(anyBoolean()); } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testRestartLayoutNotRecreatedIfNotNeeded() { - final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, - /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN); + final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); doReturn(false).when(mMockRestartDialogLayout) .needsToBeRecreated(any(TaskInfo.class), any(ShellTaskOrganizer.TaskListener.class)); - mController.onCompatInfoChanged(taskInfo, mMockTaskListener); - mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); verify(mMockRestartDialogLayout, times(1)) .createLayout(anyBoolean()); } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUpdateActiveTaskInfo_newTask_visibleAndFocused_updated() { // Simulate user aspect ratio button being shown for previous task mController.setHasShownUserAspectRatioSettingsButton(true); Assert.assertTrue(mController.hasShownUserAspectRatioSettingsButton()); // Create new task - final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, - /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN, /* isVisible */ true, - /* isFocused */ true); + final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, + /* isVisible */ true, /* isFocused */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo); @@ -545,11 +572,11 @@ public class CompatUIControllerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUpdateActiveTaskInfo_newTask_notVisibleOrFocused_notUpdated() { // Create new task - final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, - /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN, /* isVisible */ true, - /* isFocused */ true); + final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, + /* isVisible */ true, /* isFocused */ true); // Simulate task being shown mController.updateActiveTaskInfo(taskInfo); @@ -566,9 +593,8 @@ public class CompatUIControllerTest extends ShellTestCase { final int newTaskId = TASK_ID + 1; // Create visible but NOT focused task - final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId, - /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN, /* isVisible */ true, - /* isFocused */ false); + final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true, + /* isVisible */ true, /* isFocused */ false); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo1); @@ -579,9 +605,8 @@ public class CompatUIControllerTest extends ShellTestCase { Assert.assertTrue(mController.hasShownUserAspectRatioSettingsButton()); // Create focused but NOT visible task - final TaskInfo taskInfo2 = createTaskInfo(DISPLAY_ID, newTaskId, - /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN, /* isVisible */ false, - /* isFocused */ true); + final TaskInfo taskInfo2 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true, + /* isVisible */ false, /* isFocused */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo2); @@ -592,9 +617,8 @@ public class CompatUIControllerTest extends ShellTestCase { Assert.assertTrue(mController.hasShownUserAspectRatioSettingsButton()); // Create NOT focused but NOT visible task - final TaskInfo taskInfo3 = createTaskInfo(DISPLAY_ID, newTaskId, - /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN, /* isVisible */ false, - /* isFocused */ false); + final TaskInfo taskInfo3 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true, + /* isVisible */ false, /* isFocused */ false); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo3); @@ -606,11 +630,11 @@ public class CompatUIControllerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUpdateActiveTaskInfo_sameTask_notUpdated() { // Create new task - final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, - /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN, /* isVisible */ true, - /* isFocused */ true); + final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, + /* isVisible */ true, /* isFocused */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo); @@ -634,11 +658,11 @@ public class CompatUIControllerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUpdateActiveTaskInfo_transparentTask_notUpdated() { // Create new task - final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, - /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN, /* isVisible */ true, - /* isFocused */ true); + final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, + /* isVisible */ true, /* isFocused */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo); @@ -655,9 +679,8 @@ public class CompatUIControllerTest extends ShellTestCase { final int newTaskId = TASK_ID + 1; // Create transparent task - final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId, - /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN, /* isVisible */ true, - /* isFocused */ true, /* isTopActivityTransparent */ true); + final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true, + /* isVisible */ true, /* isFocused */ true, /* isTopActivityTransparent */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo1); @@ -669,45 +692,67 @@ public class CompatUIControllerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testLetterboxEduLayout_notCreatedWhenLetterboxEducationIsDisabled() { - TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - CAMERA_COMPAT_CONTROL_HIDDEN); - taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled = false; + TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); + taskInfo.appCompatTaskInfo.setLetterboxEducationEnabled(false); - mController.onCompatInfoChanged(taskInfo, mMockTaskListener); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); verify(mController, never()).createLetterboxEduWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); } - private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat, - @CameraCompatControlState int cameraCompatControlState) { - return createTaskInfo(displayId, taskId, hasSizeCompat, cameraCompatControlState, - /* isVisible */ false, /* isFocused */ false, - /* isTopActivityTransparent */ false); + @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) + @EnableFlags(Flags.FLAG_SKIP_COMPAT_UI_EDUCATION_IN_DESKTOP_MODE) + public void testUpdateActiveTaskInfo_removeAllComponentWhenInDesktopModeFlagEnabled() { + mInDesktopModePredicateResult = false; + TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); + verify(mController, never()).removeLayouts(taskInfo.taskId); + + mInDesktopModePredicateResult = true; + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); + verify(mController).removeLayouts(taskInfo.taskId); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) + @DisableFlags(Flags.FLAG_SKIP_COMPAT_UI_EDUCATION_IN_DESKTOP_MODE) + public void testUpdateActiveTaskInfo_removeAllComponentWhenInDesktopModeFlagDisabled() { + mInDesktopModePredicateResult = false; + TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); + verify(mController, never()).removeLayouts(taskInfo.taskId); + + mInDesktopModePredicateResult = true; + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); + verify(mController, never()).removeLayouts(taskInfo.taskId); + } + + private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat) { + return createTaskInfo(displayId, taskId, hasSizeCompat, /* isVisible */ false, + /* isFocused */ false, /* isTopActivityTransparent */ false); } private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat, - @CameraCompatControlState int cameraCompatControlState, boolean isVisible, - boolean isFocused) { - return createTaskInfo(displayId, taskId, hasSizeCompat, cameraCompatControlState, + boolean isVisible, boolean isFocused) { + return createTaskInfo(displayId, taskId, hasSizeCompat, isVisible, isFocused, /* isTopActivityTransparent */ false); } private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat, - @CameraCompatControlState int cameraCompatControlState, boolean isVisible, - boolean isFocused, boolean isTopActivityTransparent) { + boolean isVisible, boolean isFocused, boolean isTopActivityTransparent) { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; taskInfo.displayId = displayId; - taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat; - taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = - cameraCompatControlState; + taskInfo.appCompatTaskInfo.setTopActivityInSizeCompat(hasSizeCompat); taskInfo.isVisible = isVisible; taskInfo.isFocused = isFocused; taskInfo.isTopActivityTransparent = isTopActivityTransparent; - taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled = true; - taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed = true; + taskInfo.appCompatTaskInfo.setLetterboxEducationEnabled(true); + taskInfo.appCompatTaskInfo.setTopActivityLetterboxed(true); return taskInfo; } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java index cd3e8cb0e8e1..e5d1919decf4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java @@ -16,11 +16,6 @@ package com.android.wm.shell.compatui; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; - import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static org.mockito.Mockito.doNothing; @@ -28,9 +23,11 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; import android.app.ActivityManager; -import android.app.CameraCompatTaskInfo.CameraCompatControlState; import android.app.TaskInfo; import android.graphics.Rect; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import android.util.Pair; import android.view.LayoutInflater; @@ -40,16 +37,19 @@ import android.widget.LinearLayout; import androidx.test.filters.SmallTest; +import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; +import com.android.wm.shell.compatui.api.CompatUIEvent; import junit.framework.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -70,8 +70,12 @@ public class CompatUILayoutTest extends ShellTestCase { private static final int TASK_ID = 1; + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Mock private SyncTransactionQueue mSyncTransactionQueue; - @Mock private CompatUIController.CompatUICallback mCallback; + @Mock private Consumer<CompatUIEvent> mCallback; @Mock private Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked; @Mock private ShellTaskOrganizer.TaskListener mTaskListener; @Mock private SurfaceControlViewHost mViewHost; @@ -85,7 +89,7 @@ public class CompatUILayoutTest extends ShellTestCase { public void setUp() { MockitoAnnotations.initMocks(this); doReturn(100).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance(); - mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN); + mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false); mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue, mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(), mCompatUIConfiguration, mOnRestartButtonClicked); @@ -101,6 +105,7 @@ public class CompatUILayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnClickForRestartButton() { final ImageButton button = mLayout.findViewById(R.id.size_compat_restart_button); button.performClick(); @@ -117,6 +122,7 @@ public class CompatUILayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnLongClickForRestartButton() { doNothing().when(mWindowManager).onRestartButtonLongClicked(); @@ -127,6 +133,7 @@ public class CompatUILayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnClickForSizeCompatHint() { mWindowManager.mHasSizeCompat = true; mWindowManager.createLayout(/* canShow= */ true); @@ -136,94 +143,10 @@ public class CompatUILayoutTest extends ShellTestCase { verify(mLayout).setSizeCompatHintVisibility(/* show= */ false); } - @Test - public void testUpdateCameraTreatmentButton_treatmentAppliedByDefault() { - mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; - mWindowManager.createLayout(/* canShow= */ true); - final ImageButton button = - mLayout.findViewById(R.id.camera_compat_treatment_button); - button.performClick(); - - verify(mWindowManager).onCameraTreatmentButtonClicked(); - verify(mCallback).onCameraControlStateUpdated( - TASK_ID, CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); - - button.performClick(); - - verify(mCallback).onCameraControlStateUpdated( - TASK_ID, CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); - } - - @Test - public void testUpdateCameraTreatmentButton_treatmentSuggestedByDefault() { - mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; - mWindowManager.createLayout(/* canShow= */ true); - final ImageButton button = - mLayout.findViewById(R.id.camera_compat_treatment_button); - button.performClick(); - - verify(mWindowManager).onCameraTreatmentButtonClicked(); - verify(mCallback).onCameraControlStateUpdated( - TASK_ID, CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); - - button.performClick(); - - verify(mCallback).onCameraControlStateUpdated( - TASK_ID, CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); - } - - @Test - public void testOnCameraDismissButtonClicked() { - mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; - mWindowManager.createLayout(/* canShow= */ true); - final ImageButton button = - mLayout.findViewById(R.id.camera_compat_dismiss_button); - button.performClick(); - - verify(mWindowManager).onCameraDismissButtonClicked(); - verify(mCallback).onCameraControlStateUpdated( - TASK_ID, CAMERA_COMPAT_CONTROL_DISMISSED); - verify(mLayout).setCameraControlVisibility(/* show */ false); - } - - @Test - public void testOnLongClickForCameraTreatmentButton() { - doNothing().when(mWindowManager).onCameraButtonLongClicked(); - - final ImageButton button = - mLayout.findViewById(R.id.camera_compat_treatment_button); - button.performLongClick(); - - verify(mWindowManager).onCameraButtonLongClicked(); - } - - @Test - public void testOnLongClickForCameraDismissButton() { - doNothing().when(mWindowManager).onCameraButtonLongClicked(); - - final ImageButton button = mLayout.findViewById(R.id.camera_compat_dismiss_button); - button.performLongClick(); - - verify(mWindowManager).onCameraButtonLongClicked(); - } - - @Test - public void testOnClickForCameraCompatHint() { - mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; - mWindowManager.createLayout(/* canShow= */ true); - final LinearLayout hint = mLayout.findViewById(R.id.camera_compat_hint); - hint.performClick(); - - verify(mLayout).setCameraCompatHintVisibility(/* show= */ false); - } - - private static TaskInfo createTaskInfo(boolean hasSizeCompat, - @CameraCompatControlState int cameraCompatControlState) { + private static TaskInfo createTaskInfo(boolean hasSizeCompat) { ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); taskInfo.taskId = TASK_ID; - taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat; - taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = - cameraCompatControlState; + taskInfo.appCompatTaskInfo.setTopActivityInSizeCompat(hasSizeCompat); taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000; taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000; taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 2000, 2000)); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java new file mode 100644 index 000000000000..d6059a88e9c7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 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.compatui; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; + + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.function.IntConsumer; +import java.util.function.IntSupplier; + +/** + * Tests for {@link CompatUILayout}. + * + * Build/Install/Run: + * atest WMShellUnitTests:CompatUIStatusManagerTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class CompatUIStatusManagerTest extends ShellTestCase { + + private FakeCompatUIStatusManagerTest mTestState; + private CompatUIStatusManager mStatusManager; + + @Before + public void setUp() { + mTestState = new FakeCompatUIStatusManagerTest(); + mStatusManager = new CompatUIStatusManager(mTestState.mWriter, mTestState.mReader); + } + + @Test + public void isEducationShown() { + assertFalse(mStatusManager.isEducationVisible()); + + mStatusManager.onEducationShown(); + assertTrue(mStatusManager.isEducationVisible()); + + mStatusManager.onEducationHidden(); + assertFalse(mStatusManager.isEducationVisible()); + } + + static class FakeCompatUIStatusManagerTest { + + int mCurrentStatus = 0; + + final IntSupplier mReader = () -> mCurrentStatus; + + final IntConsumer mWriter = newStatus -> mCurrentStatus = newStatus; + + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java index 41a81c1a9921..1c0175603df9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java @@ -16,15 +16,12 @@ package com.android.wm.shell.compatui; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.view.WindowInsets.Type.navigationBars; import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -35,13 +32,16 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.app.ActivityManager; -import android.app.CameraCompatTaskInfo; import android.app.TaskInfo; import android.content.res.Configuration; import android.graphics.Rect; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.util.Pair; @@ -60,6 +60,7 @@ import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; +import com.android.wm.shell.compatui.api.CompatUIEvent; import junit.framework.Assert; @@ -85,12 +86,16 @@ public class CompatUIWindowManagerTest extends ShellTestCase { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final int TASK_ID = 1; private static final int TASK_WIDTH = 2000; private static final int TASK_HEIGHT = 2000; @Mock private SyncTransactionQueue mSyncTransactionQueue; - @Mock private CompatUIController.CompatUICallback mCallback; + @Mock private Consumer<CompatUIEvent> mCallback; @Mock private Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked; @Mock private ShellTaskOrganizer.TaskListener mTaskListener; @Mock private CompatUILayout mLayout; @@ -105,7 +110,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { public void setUp() { MockitoAnnotations.initMocks(this); doReturn(100).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance(); - mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN); + mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false); final DisplayInfo displayInfo = new DisplayInfo(); displayInfo.logicalWidth = TASK_WIDTH; @@ -129,6 +134,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testCreateSizeCompatButton() { // Doesn't create layout if show is false. mWindowManager.mHasSizeCompat = true; @@ -174,44 +180,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { } @Test - public void testCreateCameraCompatControl() { - // Doesn't create layout if show is false. - mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; - assertTrue(mWindowManager.createLayout(/* canShow= */ false)); - - verify(mWindowManager, never()).inflateLayout(); - - // Doesn't create hint popup. - mWindowManager.mCompatUIHintsState.mHasShownCameraCompatHint = true; - assertTrue(mWindowManager.createLayout(/* canShow= */ true)); - - verify(mWindowManager).inflateLayout(); - verify(mLayout).setCameraControlVisibility(/* show= */ true); - verify(mLayout, never()).setCameraCompatHintVisibility(/* show= */ true); - - // Creates hint popup. - clearInvocations(mWindowManager); - clearInvocations(mLayout); - mWindowManager.release(); - mWindowManager.mCompatUIHintsState.mHasShownCameraCompatHint = false; - assertTrue(mWindowManager.createLayout(/* canShow= */ true)); - - verify(mWindowManager).inflateLayout(); - assertNotNull(mLayout); - verify(mLayout).setCameraControlVisibility(/* show= */ true); - verify(mLayout).setCameraCompatHintVisibility(/* show= */ true); - assertTrue(mWindowManager.mCompatUIHintsState.mHasShownCameraCompatHint); - - // Returns false and doesn't create layout if Camera Compat state is hidden - clearInvocations(mWindowManager); - mWindowManager.release(); - mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_HIDDEN; - assertFalse(mWindowManager.createLayout(/* canShow= */ true)); - - verify(mWindowManager, never()).inflateLayout(); - } - - @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testRelease() { mWindowManager.mHasSizeCompat = true; mWindowManager.createLayout(/* canShow= */ true); @@ -224,13 +193,15 @@ public class CompatUIWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUpdateCompatInfo() { mWindowManager.mHasSizeCompat = true; mWindowManager.createLayout(/* canShow= */ true); + verify(mLayout).setRestartButtonVisibility(/* show= */ true); // No diff clearInvocations(mWindowManager); - TaskInfo taskInfo = createTaskInfo(/* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN); + TaskInfo taskInfo = createTaskInfo(/* hasSizeCompat= */ true); doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(any()); assertTrue(mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true)); @@ -247,58 +218,25 @@ public class CompatUIWindowManagerTest extends ShellTestCase { verify(mWindowManager).release(); verify(mWindowManager).createLayout(/* canShow= */ true); - // Change Camera Compat state, show a control. - clearInvocations(mWindowManager); - clearInvocations(mLayout); - taskInfo = createTaskInfo(/* hasSizeCompat= */ true, - CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); - assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); - - verify(mLayout).setCameraControlVisibility(/* show= */ true); - verify(mLayout).updateCameraTreatmentButton( - CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); - - // Change Camera Compat state, update a control. + // Change has Size Compat to false, no more CompatIU. clearInvocations(mWindowManager); clearInvocations(mLayout); - taskInfo = createTaskInfo(/* hasSizeCompat= */ true, - CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); - assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); - - verify(mLayout).setCameraControlVisibility(/* show= */ true); - verify(mLayout).updateCameraTreatmentButton( - CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); - - // Change has Size Compat to false, hides restart button. - clearInvocations(mWindowManager); - clearInvocations(mLayout); - taskInfo = createTaskInfo(/* hasSizeCompat= */ false, - CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); - assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); - - verify(mLayout).setRestartButtonVisibility(/* show= */ false); + taskInfo = createTaskInfo(/* hasSizeCompat= */ false); + assertFalse(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, + /* canShow= */ true)); // Change has Size Compat to true, shows restart button. clearInvocations(mWindowManager); clearInvocations(mLayout); - taskInfo = createTaskInfo(/* hasSizeCompat= */ true, - CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); - assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); - - verify(mLayout).setRestartButtonVisibility(/* show= */ true); - - // Change Camera Compat state to dismissed, hide a control. - clearInvocations(mWindowManager); - clearInvocations(mLayout); - taskInfo = createTaskInfo(/* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_DISMISSED); + taskInfo = createTaskInfo(/* hasSizeCompat= */ true); assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); - verify(mLayout).setCameraControlVisibility(/* show= */ false); + verify(mLayout, times(2)).setRestartButtonVisibility(/* show= */ true); // Change task bounds, update position. clearInvocations(mWindowManager); clearInvocations(mLayout); - taskInfo = createTaskInfo(/* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN); + taskInfo = createTaskInfo(/* hasSizeCompat= */ true); taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 1000, 0, 2000)); assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); @@ -307,7 +245,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { // Change has Size Compat to false, release layout. clearInvocations(mWindowManager); clearInvocations(mLayout); - taskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN); + taskInfo = createTaskInfo(/* hasSizeCompat= */ false); assertFalse( mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true)); @@ -315,6 +253,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUpdateCompatInfoLayoutNotInflatedYet() { mWindowManager.createLayout(/* canShow= */ false); @@ -323,15 +262,14 @@ public class CompatUIWindowManagerTest extends ShellTestCase { // Change topActivityInSizeCompat to false and pass canShow true, layout shouldn't be // inflated clearInvocations(mWindowManager); - TaskInfo taskInfo = createTaskInfo(/* hasSizeCompat= */ false, - CAMERA_COMPAT_CONTROL_HIDDEN); + TaskInfo taskInfo = createTaskInfo(/* hasSizeCompat= */ false); mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true); verify(mWindowManager, never()).inflateLayout(); // Change topActivityInSizeCompat to true and pass canShow true, layout should be inflated. clearInvocations(mWindowManager); - taskInfo = createTaskInfo(/* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN); + taskInfo = createTaskInfo(/* hasSizeCompat= */ true); mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true); verify(mWindowManager).inflateLayout(); @@ -347,6 +285,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUpdateDisplayLayout() { final DisplayInfo displayInfo = new DisplayInfo(); displayInfo.logicalWidth = 1000; @@ -366,6 +305,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUpdateDisplayLayoutInsets() { final DisplayInfo displayInfo = new DisplayInfo(); displayInfo.logicalWidth = 1000; @@ -390,6 +330,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUpdateVisibility() { // Create button if it is not created. mWindowManager.mLayout = null; @@ -415,6 +356,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testAttachToParentSurface() { final SurfaceControl.Builder b = new SurfaceControl.Builder(); mWindowManager.attachToParentSurface(b); @@ -423,37 +365,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { } @Test - public void testOnCameraDismissButtonClicked() { - mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; - mWindowManager.createLayout(/* canShow= */ true); - clearInvocations(mLayout); - mWindowManager.onCameraDismissButtonClicked(); - - verify(mCallback).onCameraControlStateUpdated(TASK_ID, CAMERA_COMPAT_CONTROL_DISMISSED); - verify(mLayout).setCameraControlVisibility(/* show= */ false); - } - - @Test - public void testOnCameraTreatmentButtonClicked() { - mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; - mWindowManager.createLayout(/* canShow= */ true); - clearInvocations(mLayout); - mWindowManager.onCameraTreatmentButtonClicked(); - - verify(mCallback).onCameraControlStateUpdated( - TASK_ID, CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); - verify(mLayout).updateCameraTreatmentButton( - CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED); - - mWindowManager.onCameraTreatmentButtonClicked(); - - verify(mCallback).onCameraControlStateUpdated( - TASK_ID, CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); - verify(mLayout).updateCameraTreatmentButton( - CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED); - } - - @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnRestartButtonClicked() { mWindowManager.onRestartButtonClicked(); @@ -468,8 +380,9 @@ public class CompatUIWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnRestartButtonLongClicked_showHint() { - // Not create hint popup. + // Not create hint popup. mWindowManager.mHasSizeCompat = true; mWindowManager.mCompatUIHintsState.mHasShownSizeCompatHint = true; mWindowManager.createLayout(/* canShow= */ true); @@ -483,21 +396,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { } @Test - public void testOnCameraControlLongClicked_showHint() { - // Not create hint popup. - mWindowManager.mCameraCompatControlState = CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; - mWindowManager.mCompatUIHintsState.mHasShownCameraCompatHint = true; - mWindowManager.createLayout(/* canShow= */ true); - - verify(mWindowManager).inflateLayout(); - verify(mLayout, never()).setCameraCompatHintVisibility(/* show= */ true); - - mWindowManager.onCameraButtonLongClicked(); - - verify(mLayout).setCameraCompatHintVisibility(/* show= */ true); - } - - @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testWhenDockedStateHasChanged_needsToBeRecreated() { ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo(); newTaskInfo.configuration.uiMode |= Configuration.UI_MODE_TYPE_DESK; @@ -506,6 +405,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testShouldShowSizeCompatRestartButton() { mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_HIDE_SCM_BUTTON); doReturn(85).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance(); @@ -514,7 +414,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { mCompatUIConfiguration, mOnRestartButtonClicked); // Simulate rotation of activity in square display - TaskInfo taskInfo = createTaskInfo(true, CAMERA_COMPAT_CONTROL_HIDDEN); + TaskInfo taskInfo = createTaskInfo(true); taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = TASK_HEIGHT; taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1850; @@ -543,13 +443,10 @@ public class CompatUIWindowManagerTest extends ShellTestCase { assertTrue(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo)); } - private static TaskInfo createTaskInfo(boolean hasSizeCompat, - @CameraCompatTaskInfo.CameraCompatControlState int cameraCompatControlState) { + private static TaskInfo createTaskInfo(boolean hasSizeCompat) { ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); taskInfo.taskId = TASK_ID; - taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat; - taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = - cameraCompatControlState; + taskInfo.appCompatTaskInfo.setTopActivityInSizeCompat(hasSizeCompat); taskInfo.configuration.uiMode &= ~Configuration.UI_MODE_TYPE_DESK; // Letterboxed activity that takes half the screen should show size compat restart button taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java index 172c263ab0f6..e8191db13084 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java @@ -16,12 +16,17 @@ package com.android.wm.shell.compatui; +import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import android.view.LayoutInflater; import android.view.View; @@ -32,6 +37,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -54,6 +60,10 @@ public class LetterboxEduDialogLayoutTest extends ShellTestCase { private View mDismissButton; private View mDialogContainer; + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -66,6 +76,7 @@ public class LetterboxEduDialogLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnFinishInflate() { assertEquals(mLayout.getDialogContainerView(), mLayout.findViewById(R.id.letterbox_education_dialog_container)); @@ -76,6 +87,7 @@ public class LetterboxEduDialogLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnDismissButtonClicked() { assertTrue(mDismissButton.performClick()); @@ -83,6 +95,7 @@ public class LetterboxEduDialogLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnBackgroundClicked() { assertTrue(mLayout.performClick()); @@ -90,6 +103,7 @@ public class LetterboxEduDialogLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnDialogContainerClicked() { assertTrue(mDialogContainer.performClick()); @@ -97,6 +111,7 @@ public class LetterboxEduDialogLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testSetDismissOnClickListenerNull() { mLayout.setDismissOnClickListener(null); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java index a60a1cbb435f..94dbd112bb75 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java @@ -19,9 +19,14 @@ package com.android.wm.shell.compatui; import static android.content.res.Configuration.UI_MODE_NIGHT_YES; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK; +import static com.android.wm.shell.compatui.CompatUIStatusManager.COMPAT_UI_EDUCATION_HIDDEN; +import static com.android.wm.shell.compatui.CompatUIStatusManager.COMPAT_UI_EDUCATION_VISIBLE; import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.assertEquals; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -37,6 +42,10 @@ import android.app.ActivityManager; import android.app.TaskInfo; import android.graphics.Insets; import android.graphics.Rect; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import android.util.Pair; import android.view.DisplayCutout; @@ -50,6 +59,7 @@ import android.view.accessibility.AccessibilityEvent; import androidx.test.filters.SmallTest; +import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; @@ -57,10 +67,12 @@ import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.compatui.CompatUIStatusManagerTest.FakeCompatUIStatusManagerTest; import com.android.wm.shell.transition.Transitions; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -115,11 +127,20 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { private CompatUIConfiguration mCompatUIConfiguration; private TestShellExecutor mExecutor; + private FakeCompatUIStatusManagerTest mCompatUIStatus; + private CompatUIStatusManager mCompatUIStatusManager; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); @Before public void setUp() { MockitoAnnotations.initMocks(this); mExecutor = new TestShellExecutor(); + mCompatUIStatus = new FakeCompatUIStatusManagerTest(); + mCompatUIStatusManager = new CompatUIStatusManager(mCompatUIStatus.mWriter, + mCompatUIStatus.mReader); mCompatUIConfiguration = new CompatUIConfiguration(mContext, mExecutor) { final Set<Integer> mHasSeenSet = new HashSet<>(); @@ -153,6 +174,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testCreateLayout_notEligible_doesNotCreateLayout() { LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ false); @@ -162,6 +184,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testCreateLayout_eligibleAndDocked_doesNotCreateLayout() { LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true, /* isDocked */ true); @@ -172,6 +195,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testCreateLayout_taskBarEducationIsShowing_doesNotCreateLayout() { LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true, USER_ID_1, /* isTaskbarEduShowing= */ true); @@ -182,6 +206,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testCreateLayout_canShowFalse_returnsTrueButDoesNotCreateLayout() { LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true); @@ -192,6 +217,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testCreateLayout_canShowTrue_createsLayoutCorrectly() { LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true); @@ -238,6 +264,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testCreateLayout_alreadyShownToUser_createsLayoutForOtherUserOnly() { LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true, USER_ID_1, /* isTaskbarEduShowing= */ false); @@ -271,6 +298,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testCreateLayout_windowManagerReleasedBeforeTransitionsIsIdle_doesNotStartAnim() { LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true); @@ -288,6 +316,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUpdateCompatInfo_updatesLayoutCorrectly() { LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true); @@ -316,6 +345,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUpdateCompatInfo_notEligibleUntilUpdate_createsLayoutAfterUpdate() { LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ false); @@ -329,6 +359,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUpdateCompatInfo_canShowFalse_doesNothing() { LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true); @@ -343,6 +374,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUpdateDisplayLayout_updatesLayoutCorrectly() { LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true); @@ -364,6 +396,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testRelease_animationIsCancelled() { LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true); @@ -374,6 +407,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testDeviceThemeChange_educationDialogUnseen_recreated() { LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true); ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo(); @@ -392,6 +426,21 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { assertFalse(windowManager.needsToBeRecreated(newTaskInfo, mTaskListener)); } + @Test + @EnableFlags(Flags.FLAG_ENABLE_COMPAT_UI_VISIBILITY_STATUS) + public void testCompatUIStatus_dialogIsShown() { + // We display the dialog + LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true, + USER_ID_1, /* isTaskbarEduShowing= */ false); + assertTrue(windowManager.createLayout(/* canShow= */ true)); + assertNotNull(windowManager.mLayout); + assertEquals(/* expected= */ COMPAT_UI_EDUCATION_VISIBLE, mCompatUIStatus.mCurrentStatus); + + // We dismiss + windowManager.release(); + assertEquals(/* expected= */ COMPAT_UI_EDUCATION_HIDDEN, mCompatUIStatus.mCurrentStatus); + } + private void verifyLayout(LetterboxEduDialogLayout layout, ViewGroup.LayoutParams params, int expectedWidth, int expectedHeight, int expectedExtraTopMargin, int expectedExtraBottomMargin) { @@ -442,7 +491,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { windowManager = new LetterboxEduWindowManager(mContext, createTaskInfo(eligible, userId), mSyncTransactionQueue, mTaskListener, createDisplayLayout(), mTransitions, mOnDismissCallback, mAnimationController, - mDockStateReader, mCompatUIConfiguration); + mDockStateReader, mCompatUIConfiguration, mCompatUIStatusManager); spyOn(windowManager); doReturn(mViewHost).when(windowManager).createSurfaceViewHost(); doReturn(isTaskbarEduShowing).when(windowManager).isTaskbarEduShowing(); @@ -477,7 +526,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); taskInfo.userId = userId; taskInfo.taskId = TASK_ID; - taskInfo.appCompatTaskInfo.topActivityEligibleForLetterboxEducation = eligible; + taskInfo.appCompatTaskInfo.setEligibleForLetterboxEducation(eligible); taskInfo.configuration.windowConfiguration.setBounds(bounds); return taskInfo; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/OWNERS new file mode 100644 index 000000000000..5b05af9b0a74 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 970984 +# includes OWNERS from parent directories
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java index 4f71b83179b1..0da14d673732 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java @@ -16,6 +16,8 @@ package com.android.wm.shell.compatui; +import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; @@ -23,6 +25,9 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.app.TaskInfo; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.LayoutInflater; @@ -34,6 +39,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -62,6 +68,10 @@ public class ReachabilityEduLayoutTest extends ShellTestCase { @Mock private TaskInfo mTaskInfo; + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -74,6 +84,7 @@ public class ReachabilityEduLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnFinishInflate() { assertNotNull(mMoveUpButton); assertNotNull(mMoveDownButton); @@ -82,6 +93,7 @@ public class ReachabilityEduLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void handleVisibility_educationNotEnabled_buttonsAreHidden() { mLayout.handleVisibility(/* horizontalEnabled */ false, /* verticalEnabled */ false, /* letterboxVerticalPosition */ @@ -94,6 +106,7 @@ public class ReachabilityEduLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void handleVisibility_horizontalEducationEnableduiConfigurationIsUpdated() { mLayout.handleVisibility(/* horizontalEnabled */ true, /* verticalEnabled */ false, /* letterboxVerticalPosition */ -1, /* letterboxHorizontalPosition */ @@ -106,6 +119,7 @@ public class ReachabilityEduLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void handleVisibility_verticalEducationEnabled_uiConfigurationIsUpdated() { mLayout.handleVisibility(/* horizontalEnabled */ false, /* verticalEnabled */ true, /* letterboxVerticalPosition */ 0, /* letterboxHorizontalPosition */ diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java index 5867a8553d53..eafb41470cda 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java @@ -16,12 +16,17 @@ package com.android.wm.shell.compatui; +import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import android.app.ActivityManager; import android.app.TaskInfo; import android.content.res.Configuration; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; @@ -35,6 +40,7 @@ import com.android.wm.shell.common.SyncTransactionQueue; import junit.framework.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -65,6 +71,10 @@ public class ReachabilityEduWindowManagerTest extends ShellTestCase { private TaskInfo mTaskInfo; private ReachabilityEduWindowManager mWindowManager; + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -80,6 +90,7 @@ public class ReachabilityEduWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testCreateLayout_notEligible_doesNotCreateLayout() { assertFalse(mWindowManager.createLayout(/* canShow= */ true)); @@ -87,6 +98,7 @@ public class ReachabilityEduWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testWhenDockedStateHasChanged_needsToBeRecreated() { ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo(); newTaskInfo.configuration.uiMode = @@ -97,6 +109,7 @@ public class ReachabilityEduWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testWhenDarkLightThemeHasChanged_needsToBeRecreated() { ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo(); mTaskInfo.configuration.uiMode = diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java index e2dcdb0e91b2..6b0c5dd2e1c7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java @@ -16,6 +16,8 @@ package com.android.wm.shell.compatui; +import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -23,6 +25,9 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import android.view.LayoutInflater; import android.view.View; @@ -34,6 +39,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -60,6 +66,10 @@ public class RestartDialogLayoutTest extends ShellTestCase { private View mDialogContainer; private CheckBox mDontRepeatCheckBox; + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -76,6 +86,7 @@ public class RestartDialogLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnFinishInflate() { assertEquals(mLayout.getDialogContainerView(), mLayout.findViewById(R.id.letterbox_restart_dialog_container)); @@ -86,6 +97,7 @@ public class RestartDialogLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnDismissButtonClicked() { assertTrue(mDismissButton.performClick()); @@ -93,6 +105,7 @@ public class RestartDialogLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnRestartButtonClickedWithoutCheckbox() { mDontRepeatCheckBox.setChecked(false); assertTrue(mRestartButton.performClick()); @@ -101,6 +114,7 @@ public class RestartDialogLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnRestartButtonClickedWithCheckbox() { mDontRepeatCheckBox.setChecked(true); assertTrue(mRestartButton.performClick()); @@ -109,6 +123,7 @@ public class RestartDialogLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnBackgroundClickedDoesntDismiss() { assertFalse(mLayout.performClick()); @@ -116,6 +131,7 @@ public class RestartDialogLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnDialogContainerClicked() { assertTrue(mDialogContainer.performClick()); @@ -124,6 +140,7 @@ public class RestartDialogLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testSetDismissOnClickListenerNull() { mLayout.setDismissOnClickListener(null); @@ -135,6 +152,7 @@ public class RestartDialogLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testSetRestartOnClickListenerNull() { mLayout.setRestartOnClickListener(null); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java index 9f109a1d0f50..cfeef90cb4b6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java @@ -16,9 +16,14 @@ package com.android.wm.shell.compatui; +import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK; + import android.app.ActivityManager; import android.app.TaskInfo; import android.content.res.Configuration; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import android.util.Pair; @@ -33,6 +38,7 @@ import com.android.wm.shell.transition.Transitions; import junit.framework.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -60,6 +66,10 @@ public class RestartDialogWindowManagerTest extends ShellTestCase { private RestartDialogWindowManager mWindowManager; private TaskInfo mTaskInfo; + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -76,6 +86,7 @@ public class RestartDialogWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testWhenDockedStateHasChanged_needsToBeRecreated() { ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo(); newTaskInfo.configuration.uiMode = @@ -86,6 +97,7 @@ public class RestartDialogWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testWhenDarkLightThemeHasChanged_needsToBeRecreated() { ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo(); mTaskInfo.configuration.uiMode = diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java index 02316125bcc3..e8e68bdbd940 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java @@ -16,18 +16,19 @@ package com.android.wm.shell.compatui; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; - import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; import android.app.ActivityManager; -import android.app.CameraCompatTaskInfo.CameraCompatControlState; import android.app.TaskInfo; import android.content.ComponentName; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import android.util.Pair; import android.view.LayoutInflater; @@ -47,6 +48,7 @@ import com.android.wm.shell.common.SyncTransactionQueue; import junit.framework.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -86,10 +88,14 @@ public class UserAspectRatioSettingsLayoutTest extends ShellTestCase { private UserAspectRatioSettingsLayout mLayout; private TaskInfo mTaskInfo; + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setUp() { MockitoAnnotations.initMocks(this); - mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN); + mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false); mWindowManager = new UserAspectRatioSettingsWindowManager(mContext, mTaskInfo, mSyncTransactionQueue, mTaskListener, new DisplayLayout(), new CompatUIController.CompatUIHintsState(), @@ -107,6 +113,7 @@ public class UserAspectRatioSettingsLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnClickForUserAspectRatioSettingsButton() { final ImageButton button = mLayout.findViewById(R.id.user_aspect_ratio_settings_button); button.performClick(); @@ -123,6 +130,7 @@ public class UserAspectRatioSettingsLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnLongClickForUserAspectRatioButton() { doNothing().when(mWindowManager).onUserAspectRatioSettingsButtonLongClicked(); @@ -133,6 +141,7 @@ public class UserAspectRatioSettingsLayoutTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnClickForUserAspectRatioSettingsHint() { mWindowManager.mHasUserAspectRatioSettingsButton = true; mWindowManager.createLayout(/* canShow= */ true); @@ -143,13 +152,10 @@ public class UserAspectRatioSettingsLayoutTest extends ShellTestCase { verify(mLayout).setUserAspectRatioSettingsHintVisibility(/* show= */ false); } - private static TaskInfo createTaskInfo(boolean hasSizeCompat, - @CameraCompatControlState int cameraCompatControlState) { + private static TaskInfo createTaskInfo(boolean hasSizeCompat) { ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); taskInfo.taskId = TASK_ID; - taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat; - taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState = - cameraCompatControlState; + taskInfo.appCompatTaskInfo.setTopActivityInSizeCompat(hasSizeCompat); taskInfo.realActivity = new ComponentName("com.mypackage.test", "TestActivity"); return taskInfo; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java index 94e168ed70ed..9f86d49b52c4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java @@ -22,6 +22,7 @@ import static android.hardware.usb.UsbManager.ACTION_USB_STATE; import static android.view.WindowInsets.Type.navigationBars; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -39,6 +40,9 @@ import android.content.ComponentName; import android.content.Intent; import android.content.res.Configuration; import android.graphics.Rect; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; import android.util.Pair; @@ -61,6 +65,7 @@ import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; import junit.framework.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -107,6 +112,10 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { private TestShellExecutor mExecutor; + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -138,6 +147,7 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testCreateUserAspectRatioButton() { // Doesn't create layout if show is false. mWindowManager.mHasUserAspectRatioSettingsButton = true; @@ -178,6 +188,7 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testRelease() { mWindowManager.mHasUserAspectRatioSettingsButton = true; mWindowManager.createLayout(/* canShow= */ true); @@ -190,6 +201,7 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUpdateCompatInfo() { mWindowManager.mHasUserAspectRatioSettingsButton = true; mWindowManager.createLayout(/* canShow= */ true); @@ -242,6 +254,7 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUpdateCompatInfoLayoutNotInflatedYet() { mWindowManager.mHasUserAspectRatioSettingsButton = true; mWindowManager.createLayout(/* canShow= */ false); @@ -267,6 +280,7 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testEligibleButtonHiddenIfLetterboxBoundsEqualToStableBounds() { TaskInfo taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */ true, /* topActivityBoundsLetterboxed */ true, ACTION_MAIN, CATEGORY_LAUNCHER); @@ -292,6 +306,7 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUserFullscreenOverrideEnabled_buttonAlwaysShown() { TaskInfo taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */ true, /* topActivityBoundsLetterboxed */ true, ACTION_MAIN, CATEGORY_LAUNCHER); @@ -302,7 +317,7 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { // layout should be inflated taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = stableBounds.height(); taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = stableBounds.width(); - taskInfo.appCompatTaskInfo.isUserFullscreenOverrideEnabled = true; + taskInfo.appCompatTaskInfo.setUserFullscreenOverrideEnabled(true); mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true); @@ -310,6 +325,7 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUpdateDisplayLayout() { final DisplayInfo displayInfo = new DisplayInfo(); displayInfo.logicalWidth = 1000; @@ -329,6 +345,7 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUpdateDisplayLayoutInsets() { final DisplayInfo displayInfo = new DisplayInfo(); displayInfo.logicalWidth = 1000; @@ -353,6 +370,7 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testUpdateVisibility() { // Create button if it is not created. mWindowManager.removeLayout(); @@ -378,6 +396,7 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testLayoutHasUserAspectRatioSettingsButton() { clearInvocations(mWindowManager); spyOn(mWindowManager); @@ -411,6 +430,7 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testAttachToParentSurface() { final SurfaceControl.Builder b = new SurfaceControl.Builder(); mWindowManager.attachToParentSurface(b); @@ -419,6 +439,7 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnUserAspectRatioButtonClicked() { mWindowManager.onUserAspectRatioSettingsButtonClicked(); @@ -433,6 +454,7 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testOnUserAspectRatioButtonLongClicked_showHint() { // Not create hint popup. mWindowManager.mHasUserAspectRatioSettingsButton = true; @@ -448,6 +470,7 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) public void testWhenDockedStateHasChanged_needsToBeRecreated() { ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo(); newTaskInfo.configuration.uiMode |= Configuration.UI_MODE_TYPE_DESK; @@ -459,9 +482,9 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { boolean topActivityBoundsLetterboxed, String action, String category) { ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); taskInfo.taskId = TASK_ID; - taskInfo.appCompatTaskInfo.topActivityEligibleForUserAspectRatioButton = - eligibleForUserAspectRatioButton; - taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed = topActivityBoundsLetterboxed; + taskInfo.appCompatTaskInfo.setEligibleForUserAspectRatioButton( + eligibleForUserAspectRatioButton); + taskInfo.appCompatTaskInfo.setTopActivityLetterboxed(topActivityBoundsLetterboxed); taskInfo.configuration.uiMode &= ~Configuration.UI_MODE_TYPE_DESK; taskInfo.realActivity = new ComponentName("com.mypackage.test", "TestActivity"); taskInfo.baseIntent = new Intent(action).addCategory(category); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/CompatUIComponentTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/CompatUIComponentTest.kt new file mode 100644 index 000000000000..2c203c466c6e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/CompatUIComponentTest.kt @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2024 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.compatui.impl + +import android.app.ActivityManager +import android.graphics.Point +import android.testing.AndroidTestingRunner +import android.view.View +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.compatui.api.CompatUIComponent +import com.android.wm.shell.compatui.api.CompatUIComponentState +import com.android.wm.shell.compatui.api.CompatUIInfo +import com.android.wm.shell.compatui.api.CompatUIState +import junit.framework.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +/** + * Tests for {@link CompatUIComponent}. + * + * Build/Install/Run: + * atest WMShellUnitTests:CompatUIComponentTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class CompatUIComponentTest : ShellTestCase() { + + private lateinit var component: CompatUIComponent + private lateinit var layout: FakeCompatUILayout + private lateinit var spec: FakeCompatUISpec + private lateinit var state: CompatUIState + private lateinit var info: CompatUIInfo + private lateinit var syncQueue: SyncTransactionQueue + private lateinit var displayLayout: DisplayLayout + private lateinit var view: View + private lateinit var position: Point + private lateinit var componentState: CompatUIComponentState + + @JvmField + @Rule + val compatUIHandlerRule: CompatUIHandlerRule = CompatUIHandlerRule() + + @Before + fun setUp() { + state = CompatUIState() + view = View(mContext) + position = Point(123, 456) + layout = FakeCompatUILayout(viewBuilderReturn = view, positionBuilderReturn = position) + spec = FakeCompatUISpec("comp", layout = layout) + info = testCompatUIInfo() + syncQueue = mock<SyncTransactionQueue>() + displayLayout = mock<DisplayLayout>() + component = + CompatUIComponent(spec.getSpec(), + "compId", + mContext, + state, + info, + syncQueue, + displayLayout) + componentState = object : CompatUIComponentState {} + state.registerUIComponent("compId", component, componentState) + } + + @Test + fun `when initLayout is invoked spec fields are used`() { + compatUIHandlerRule.postBlocking { + component.initLayout(info) + } + with(layout) { + assertViewBuilderInvocation(1) + assertEquals(info, lastViewBuilderCompatUIInfo) + assertEquals(componentState, lastViewBuilderCompState) + assertViewBinderInvocation(0) + assertPositionFactoryInvocation(1) + assertEquals(info, lastPositionFactoryCompatUIInfo) + assertEquals(view, lastPositionFactoryView) + assertEquals(componentState, lastPositionFactoryCompState) + assertEquals(state.sharedState, lastPositionFactorySharedState) + } + } + + @Test + fun `when update is invoked only position and binder spec fields are used`() { + compatUIHandlerRule.postBlocking { + component.initLayout(info) + layout.resetState() + component.update(info) + } + with(layout) { + assertViewBuilderInvocation(0) + assertViewBinderInvocation(1) + assertPositionFactoryInvocation(1) + } + } + + private fun testCompatUIInfo(): CompatUIInfo { + val taskInfo = ActivityManager.RunningTaskInfo() + taskInfo.taskId = 1 + return CompatUIInfo(taskInfo, null) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/CompatUIHandlerRule.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/CompatUIHandlerRule.kt new file mode 100644 index 000000000000..4b8b65c784e9 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/CompatUIHandlerRule.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 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.compatui.impl + +import android.os.HandlerThread +import java.util.concurrent.CountDownLatch +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Utility {@link TestRule} to manage Handlers in Compat UI tests. + */ +class CompatUIHandlerRule : TestRule { + + private lateinit var handler: HandlerThread + + /** + * Makes the HandlerThread available during the test + */ + override fun apply(base: Statement?, description: Description?): Statement { + handler = HandlerThread("CompatUIHandler").apply { + start() + } + return object : Statement() { + @Throws(Throwable::class) + override fun evaluate() { + try { + base!!.evaluate() + } finally { + handler.quitSafely() + } + } + } + } + + /** + * Posts a {@link Runnable} for the Handler + * @param runnable The Runnable to execute + */ + fun postBlocking(runnable: Runnable) { + val countDown = CountDownLatch(/* count = */ 1) + handler.threadHandler.post{ + runnable.run() + countDown.countDown() + } + try { + countDown.await() + } catch (e: InterruptedException) { + // No-op + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/CompatUIStateUtil.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/CompatUIStateUtil.kt new file mode 100644 index 000000000000..4f0e5b9cbfbf --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/CompatUIStateUtil.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 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.compatui.impl + +import com.android.wm.shell.compatui.api.CompatUIComponentState +import com.android.wm.shell.compatui.api.CompatUIState +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertNotNull +import junit.framework.Assert.assertNull + +/** + * Asserts no component state exists for the given CompatUISpec + */ +internal fun CompatUIState.assertHasNoStateFor(componentId: String) = + assertNull(stateForComponent(componentId)) + +/** + * Asserts component state for the given CompatUISpec + */ +internal fun CompatUIState.assertHasStateEqualsTo( + componentId: String, + expected: CompatUIComponentState +) = + assertEquals(stateForComponent(componentId), expected) + +/** + * Asserts no component exists for the given CompatUISpec + */ +internal fun CompatUIState.assertHasNoComponentFor(componentId: String) = + assertNull(getUIComponent(componentId)) + +/** + * Asserts component for the given CompatUISpec + */ +internal fun CompatUIState.assertHasComponentFor(componentId: String) = + assertNotNull(getUIComponent(componentId)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandlerTest.kt new file mode 100644 index 000000000000..66852ad5ab5d --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandlerTest.kt @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2024 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.compatui.impl + +import android.app.ActivityManager +import android.testing.AndroidTestingRunner +import android.view.View +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +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.SyncTransactionQueue +import com.android.wm.shell.compatui.api.CompatUIComponentState +import com.android.wm.shell.compatui.api.CompatUIInfo +import com.android.wm.shell.compatui.api.CompatUIState +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +/** + * Tests for {@link DefaultCompatUIHandler}. + * + * Build/Install/Run: + * atest WMShellUnitTests:DefaultCompatUIHandlerTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class DefaultCompatUIHandlerTest : ShellTestCase() { + + @JvmField + @Rule + val compatUIHandlerRule: CompatUIHandlerRule = CompatUIHandlerRule() + + lateinit var compatUIRepository: FakeCompatUIRepository + lateinit var compatUIHandler: DefaultCompatUIHandler + lateinit var compatUIState: CompatUIState + lateinit var fakeIdGenerator: FakeCompatUIComponentIdGenerator + lateinit var syncQueue: SyncTransactionQueue + lateinit var displayController: DisplayController + lateinit var shellExecutor: TestShellExecutor + lateinit var componentFactory: FakeCompatUIComponentFactory + + @Before + fun setUp() { + shellExecutor = TestShellExecutor() + compatUIRepository = FakeCompatUIRepository() + compatUIState = CompatUIState() + fakeIdGenerator = FakeCompatUIComponentIdGenerator("compId") + syncQueue = mock<SyncTransactionQueue>() + displayController = mock<DisplayController>() + componentFactory = FakeCompatUIComponentFactory(mContext, syncQueue, displayController) + compatUIHandler = + DefaultCompatUIHandler( + compatUIRepository, + compatUIState, + fakeIdGenerator, + componentFactory, + shellExecutor) + } + + @Test + fun `when creationReturn is false no state is stored`() { + // We add a spec to the repository + val fakeLifecycle = FakeCompatUILifecyclePredicates( + creationReturn = false, + removalReturn = false + ) + val fakeCompatUILayout = FakeCompatUILayout(viewBuilderReturn = View(mContext)) + val fakeCompatUISpec = + FakeCompatUISpec(name = "one", + lifecycle = fakeLifecycle, + layout = fakeCompatUILayout).getSpec() + compatUIRepository.addSpec(fakeCompatUISpec) + + val generatedId = fakeIdGenerator.generatedComponentId + + compatUIHandlerRule.postBlocking { + compatUIHandler.onCompatInfoChanged(testCompatUIInfo()) + } + + fakeIdGenerator.assertGenerateInvocations(1) + fakeLifecycle.assertCreationInvocation(1) + fakeLifecycle.assertRemovalInvocation(0) + fakeLifecycle.assertInitialStateInvocation(0) + compatUIState.assertHasNoStateFor(generatedId) + compatUIState.assertHasNoComponentFor(generatedId) + + compatUIHandlerRule.postBlocking { + compatUIHandler.onCompatInfoChanged(testCompatUIInfo()) + } + fakeLifecycle.assertCreationInvocation(2) + fakeLifecycle.assertRemovalInvocation(0) + fakeLifecycle.assertInitialStateInvocation(0) + compatUIState.assertHasNoStateFor(generatedId) + compatUIState.assertHasNoComponentFor(generatedId) + } + + @Test + fun `when creationReturn is true and no state is created no state is stored`() { + // We add a spec to the repository + val fakeLifecycle = FakeCompatUILifecyclePredicates( + creationReturn = true, + removalReturn = false + ) + val fakeCompatUILayout = FakeCompatUILayout(viewBuilderReturn = View(mContext)) + val fakeCompatUISpec = + FakeCompatUISpec(name = "one", + lifecycle = fakeLifecycle, + layout = fakeCompatUILayout).getSpec() + compatUIRepository.addSpec(fakeCompatUISpec) + + val generatedId = fakeIdGenerator.generatedComponentId + + compatUIHandlerRule.postBlocking { + compatUIHandler.onCompatInfoChanged(testCompatUIInfo()) + } + + fakeLifecycle.assertCreationInvocation(1) + fakeLifecycle.assertRemovalInvocation(0) + fakeLifecycle.assertInitialStateInvocation(1) + compatUIState.assertHasNoStateFor(generatedId) + compatUIState.assertHasComponentFor(generatedId) + + compatUIHandlerRule.postBlocking { + compatUIHandler.onCompatInfoChanged(testCompatUIInfo()) + } + + fakeLifecycle.assertCreationInvocation(1) + fakeLifecycle.assertRemovalInvocation(1) + fakeLifecycle.assertInitialStateInvocation(1) + compatUIState.assertHasNoStateFor(generatedId) + compatUIState.assertHasComponentFor(generatedId) + } + + @Test + fun `when creationReturn is true and state is created state is stored`() { + val fakeComponentState = object : CompatUIComponentState {} + // We add a spec to the repository + val fakeLifecycle = FakeCompatUILifecyclePredicates( + creationReturn = true, + removalReturn = false, + initialState = { _, _ -> fakeComponentState } + ) + val fakeCompatUILayout = FakeCompatUILayout(viewBuilderReturn = View(mContext)) + val fakeCompatUISpec = + FakeCompatUISpec(name = "one", + lifecycle = fakeLifecycle, + layout = fakeCompatUILayout).getSpec() + compatUIRepository.addSpec(fakeCompatUISpec) + + val generatedId = fakeIdGenerator.generatedComponentId + + compatUIHandlerRule.postBlocking { + compatUIHandler.onCompatInfoChanged(testCompatUIInfo()) + } + + fakeLifecycle.assertCreationInvocation(1) + fakeLifecycle.assertRemovalInvocation(0) + fakeLifecycle.assertInitialStateInvocation(1) + compatUIState.assertHasStateEqualsTo(generatedId, fakeComponentState) + compatUIState.assertHasComponentFor(generatedId) + + compatUIHandlerRule.postBlocking { + compatUIHandler.onCompatInfoChanged(testCompatUIInfo()) + } + + fakeLifecycle.assertCreationInvocation(1) + fakeLifecycle.assertRemovalInvocation(1) + fakeLifecycle.assertInitialStateInvocation(1) + compatUIState.assertHasStateEqualsTo(generatedId, fakeComponentState) + compatUIState.assertHasComponentFor(generatedId) + } + + @Test + fun `when lifecycle is complete and state is created state is stored and removed`() { + val fakeComponentState = object : CompatUIComponentState {} + // We add a spec to the repository + val fakeLifecycle = FakeCompatUILifecyclePredicates( + creationReturn = true, + removalReturn = true, + initialState = { _, _ -> fakeComponentState } + ) + val fakeCompatUILayout = FakeCompatUILayout(viewBuilderReturn = View(mContext)) + val fakeCompatUISpec = + FakeCompatUISpec(name = "one", + lifecycle = fakeLifecycle, + layout = fakeCompatUILayout).getSpec() + compatUIRepository.addSpec(fakeCompatUISpec) + + val generatedId = fakeIdGenerator.generatedComponentId + + compatUIHandlerRule.postBlocking { + compatUIHandler.onCompatInfoChanged(testCompatUIInfo()) + } + + fakeLifecycle.assertCreationInvocation(1) + fakeLifecycle.assertRemovalInvocation(0) + fakeLifecycle.assertInitialStateInvocation(1) + compatUIState.assertHasStateEqualsTo(generatedId, fakeComponentState) + compatUIState.assertHasComponentFor(generatedId) + + compatUIHandlerRule.postBlocking { + compatUIHandler.onCompatInfoChanged(testCompatUIInfo()) + } + + fakeLifecycle.assertCreationInvocation(1) + fakeLifecycle.assertRemovalInvocation(1) + fakeLifecycle.assertInitialStateInvocation(1) + compatUIState.assertHasNoStateFor(generatedId) + compatUIState.assertHasNoComponentFor(generatedId) + } + + @Test + fun `idGenerator is invoked every time a component is created`() { + // We add a spec to the repository + val fakeLifecycle = FakeCompatUILifecyclePredicates( + creationReturn = true, + removalReturn = true, + ) + val fakeCompatUILayout = FakeCompatUILayout(viewBuilderReturn = View(mContext)) + val fakeCompatUISpec = FakeCompatUISpec("one", fakeLifecycle, + fakeCompatUILayout).getSpec() + compatUIRepository.addSpec(fakeCompatUISpec) + // Component creation + fakeIdGenerator.assertGenerateInvocations(0) + compatUIHandlerRule.postBlocking { + compatUIHandler.onCompatInfoChanged(testCompatUIInfo()) + } + fakeIdGenerator.assertGenerateInvocations(1) + + compatUIHandlerRule.postBlocking { + compatUIHandler.onCompatInfoChanged(testCompatUIInfo()) + } + fakeIdGenerator.assertGenerateInvocations(2) + } + + @Test + fun `viewBuilder and viewBinder invoked if component is created and released when destroyed`() { + // We add a spec to the repository + val fakeLifecycle = FakeCompatUILifecyclePredicates( + creationReturn = true, + removalReturn = true, + ) + val fakeCompatUILayout = FakeCompatUILayout(viewBuilderReturn = View(mContext)) + val fakeCompatUISpec = FakeCompatUISpec("one", fakeLifecycle, + fakeCompatUILayout).getSpec() + compatUIRepository.addSpec(fakeCompatUISpec) + + compatUIHandlerRule.postBlocking { + compatUIHandler.onCompatInfoChanged(testCompatUIInfo()) + } + shellExecutor.flushAll() + componentFactory.assertInvocations(1) + fakeCompatUILayout.assertViewBuilderInvocation(1) + fakeCompatUILayout.assertViewBinderInvocation(1) + fakeCompatUILayout.assertViewReleaserInvocation(0) + + compatUIHandlerRule.postBlocking { + compatUIHandler.onCompatInfoChanged(testCompatUIInfo()) + } + shellExecutor.flushAll() + + componentFactory.assertInvocations(1) + fakeCompatUILayout.assertViewBuilderInvocation(1) + fakeCompatUILayout.assertViewBinderInvocation(1) + fakeCompatUILayout.assertViewReleaserInvocation(1) + } + + + private fun testCompatUIInfo(): CompatUIInfo { + val taskInfo = ActivityManager.RunningTaskInfo() + taskInfo.taskId = 1 + return CompatUIInfo(taskInfo, null) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepositoryTest.kt new file mode 100644 index 000000000000..319122d1e051 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepositoryTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2024 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.compatui.impl + + +import android.graphics.Point +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import android.testing.AndroidTestingRunner +import android.view.View +import androidx.test.filters.SmallTest +import com.android.wm.shell.compatui.api.CompatUILayout +import com.android.wm.shell.compatui.api.CompatUILifecyclePredicates +import com.android.wm.shell.compatui.api.CompatUIRepository +import com.android.wm.shell.compatui.api.CompatUISpec +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for {@link DefaultCompatUIRepository}. + * + * Build/Install/Run: + * atest WMShellUnitTests:DefaultCompatUIRepositoryTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class DefaultCompatUIRepositoryTest { + + lateinit var repository: CompatUIRepository + + @get:Rule + val mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + @Before + fun setUp() { + repository = DefaultCompatUIRepository() + } + + @Test(expected = IllegalStateException::class) + fun `addSpec throws exception with specs with duplicate id`() { + repository.addSpec(specById("one")) + repository.addSpec(specById("one")) + } + + @Test + fun `iterateOn invokes the consumer`() { + with(repository) { + addSpec(specById("one")) + addSpec(specById("two")) + addSpec(specById("three")) + val consumer = object : (CompatUISpec) -> Unit { + var acc = "" + override fun invoke(spec: CompatUISpec) { + acc += spec.name + } + } + iterateOn(consumer) + assertEquals("onetwothree", consumer.acc) + } + } + + @Test + fun `findSpec returns existing specs`() { + with(repository) { + val one = specById("one") + val two = specById("two") + val three = specById("three") + addSpec(one) + addSpec(two) + addSpec(three) + assertEquals(findSpec("one"), one) + assertEquals(findSpec("two"), two) + assertEquals(findSpec("three"), three) + assertNull(findSpec("abc")) + } + } + + private fun specById(name: String): CompatUISpec = + CompatUISpec(name = name, + lifecycle = CompatUILifecyclePredicates( + creationPredicate = { _, _ -> true }, + removalPredicate = { _, _, _ -> true } + ), + layout = CompatUILayout( + viewBuilder = { ctx, _, _ -> View(ctx) }, + positionFactory = { _, _, _, _ -> Point(0, 0) } + ) + ) +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUIComponentFactory.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUIComponentFactory.kt new file mode 100644 index 000000000000..782add84a36c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUIComponentFactory.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 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.compatui.impl + +import android.content.Context +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.compatui.api.CompatUIComponent +import com.android.wm.shell.compatui.api.CompatUIComponentFactory +import com.android.wm.shell.compatui.api.CompatUIInfo +import com.android.wm.shell.compatui.api.CompatUISpec +import com.android.wm.shell.compatui.api.CompatUIState +import junit.framework.Assert.assertEquals + +/** + * Fake {@link CompatUIComponentFactory} implementation. + */ +class FakeCompatUIComponentFactory( + private val context: Context, + private val syncQueue: SyncTransactionQueue, + private val displayController: DisplayController +) : CompatUIComponentFactory { + + var lastSpec: CompatUISpec? = null + var lastCompId: String? = null + var lastState: CompatUIState? = null + var lastInfo: CompatUIInfo? = null + + var numberInvocations = 0 + + override fun create( + spec: CompatUISpec, + compId: String, + state: CompatUIState, + compatUIInfo: CompatUIInfo + ): CompatUIComponent { + lastSpec = spec + lastCompId = compId + lastState = state + lastInfo = compatUIInfo + numberInvocations++ + return CompatUIComponent( + spec, + compId, + context, + state, + compatUIInfo, + syncQueue, + displayController.getDisplayLayout(compatUIInfo.taskInfo.displayId) + ) + } + + fun assertInvocations(expected: Int) = + assertEquals(expected, numberInvocations) +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUIComponentIdGenerator.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUIComponentIdGenerator.kt new file mode 100644 index 000000000000..bc743edc465d --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUIComponentIdGenerator.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 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.compatui.impl + +import com.android.wm.shell.compatui.api.CompatUIComponentIdGenerator +import com.android.wm.shell.compatui.api.CompatUIInfo +import com.android.wm.shell.compatui.api.CompatUISpec +import junit.framework.Assert.assertEquals + +/** + * A Fake {@link CompatUIComponentIdGenerator} implementation. + */ +class FakeCompatUIComponentIdGenerator(var generatedComponentId: String = "compId") : + CompatUIComponentIdGenerator { + + var generateInvocations = 0 + + override fun generateId(compatUIInfo: CompatUIInfo, spec: CompatUISpec): String { + generateInvocations++ + return generatedComponentId + } + + fun resetInvocations() { + generateInvocations = 0 + } + + fun assertGenerateInvocations(expected: Int) = + assertEquals(expected, generateInvocations) +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUILayout.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUILayout.kt new file mode 100644 index 000000000000..d7a178ab69e8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUILayout.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2024 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.compatui.impl + +import android.content.Context +import android.graphics.Point +import android.view.View +import com.android.wm.shell.compatui.api.CompatUIComponentState +import com.android.wm.shell.compatui.api.CompatUIInfo +import com.android.wm.shell.compatui.api.CompatUILayout +import com.android.wm.shell.compatui.api.CompatUISharedState +import junit.framework.Assert.assertEquals + +/** + * Fake class for {@link CompatUILayout} + */ +class FakeCompatUILayout( + private val zOrderReturn: Int = 0, + private val layoutParamFlagsReturn: Int = 0, + private val viewBuilderReturn: View, + private val positionBuilderReturn: Point = Point(0, 0) +) { + + var viewBuilderInvocation = 0 + var viewBinderInvocation = 0 + var positionFactoryInvocation = 0 + var viewReleaserInvocation = 0 + + var lastViewBuilderContext: Context? = null + var lastViewBuilderCompatUIInfo: CompatUIInfo? = null + var lastViewBuilderCompState: CompatUIComponentState? = null + var lastViewBinderView: View? = null + var lastViewBinderCompatUIInfo: CompatUIInfo? = null + var lastViewBinderSharedState: CompatUISharedState? = null + var lastViewBinderCompState: CompatUIComponentState? = null + var lastPositionFactoryView: View? = null + var lastPositionFactoryCompatUIInfo: CompatUIInfo? = null + var lastPositionFactorySharedState: CompatUISharedState? = null + var lastPositionFactoryCompState: CompatUIComponentState? = null + + fun getLayout() = CompatUILayout( + zOrder = zOrderReturn, + layoutParamFlags = layoutParamFlagsReturn, + viewBuilder = { ctx, info, componentState -> + lastViewBuilderContext = ctx + lastViewBuilderCompatUIInfo = info + lastViewBuilderCompState = componentState + viewBuilderInvocation++ + viewBuilderReturn + }, + viewBinder = { view, info, sharedState, componentState -> + lastViewBinderView = view + lastViewBinderCompatUIInfo = info + lastViewBinderCompState = componentState + lastViewBinderSharedState = sharedState + viewBinderInvocation++ + }, + positionFactory = { view, info, sharedState, componentState -> + lastPositionFactoryView = view + lastPositionFactoryCompatUIInfo = info + lastPositionFactoryCompState = componentState + lastPositionFactorySharedState = sharedState + positionFactoryInvocation++ + positionBuilderReturn + }, + viewReleaser = { viewReleaserInvocation++ } + ) + + fun assertViewBuilderInvocation(expected: Int) = + assertEquals(expected, viewBuilderInvocation) + + fun assertViewBinderInvocation(expected: Int) = + assertEquals(expected, viewBinderInvocation) + + fun assertViewReleaserInvocation(expected: Int) = + assertEquals(expected, viewReleaserInvocation) + + fun assertPositionFactoryInvocation(expected: Int) = + assertEquals(expected, positionFactoryInvocation) + + fun resetState() { + viewBuilderInvocation = 0 + viewBinderInvocation = 0 + positionFactoryInvocation = 0 + viewReleaserInvocation = 0 + lastViewBuilderCompatUIInfo = null + lastViewBuilderCompState = null + lastViewBinderView = null + lastViewBinderCompatUIInfo = null + lastViewBinderSharedState = null + lastViewBinderCompState = null + lastPositionFactoryView = null + lastPositionFactoryCompatUIInfo = null + lastPositionFactorySharedState = null + lastPositionFactoryCompState = null + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUILifecyclePredicates.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUILifecyclePredicates.kt new file mode 100644 index 000000000000..f742ca32e63d --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUILifecyclePredicates.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 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.compatui.impl + +import com.android.wm.shell.compatui.api.CompatUIComponentState +import com.android.wm.shell.compatui.api.CompatUIInfo +import com.android.wm.shell.compatui.api.CompatUILifecyclePredicates +import com.android.wm.shell.compatui.api.CompatUISharedState +import junit.framework.Assert.assertEquals + +/** + * Fake class for {@link CompatUILifecycle} + */ +class FakeCompatUILifecyclePredicates( + private val creationReturn: Boolean = false, + private val removalReturn: Boolean = false, + private val initialState: ( + CompatUIInfo, + CompatUISharedState + ) -> CompatUIComponentState? = { _, _ -> null } +) { + var creationInvocation = 0 + var removalInvocation = 0 + var initialStateInvocation = 0 + var lastCreationCompatUIInfo: CompatUIInfo? = null + var lastCreationSharedState: CompatUISharedState? = null + var lastRemovalCompatUIInfo: CompatUIInfo? = null + var lastRemovalSharedState: CompatUISharedState? = null + var lastRemovalCompState: CompatUIComponentState? = null + fun getLifecycle() = CompatUILifecyclePredicates( + creationPredicate = { uiInfo, sharedState -> + lastCreationCompatUIInfo = uiInfo + lastCreationSharedState = sharedState + creationInvocation++ + creationReturn + }, + removalPredicate = { uiInfo, sharedState, compState -> + lastRemovalCompatUIInfo = uiInfo + lastRemovalSharedState = sharedState + lastRemovalCompState = compState + removalInvocation++ + removalReturn + }, + stateBuilder = { a, b -> initialStateInvocation++; initialState(a, b) } + ) + + fun assertCreationInvocation(expected: Int) = + assertEquals(expected, creationInvocation) + + fun assertRemovalInvocation(expected: Int) = + assertEquals(expected, removalInvocation) + + fun assertInitialStateInvocation(expected: Int) = + assertEquals(expected, initialStateInvocation) +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUIRepository.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUIRepository.kt new file mode 100644 index 000000000000..cdc524aff09f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUIRepository.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 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.compatui.impl + +import com.android.wm.shell.compatui.api.CompatUIRepository +import com.android.wm.shell.compatui.api.CompatUISpec + +/** + * Fake implementation for {@link CompatUIRepository} + */ +class FakeCompatUIRepository : CompatUIRepository { + val allSpecs = mutableMapOf<String, CompatUISpec>() + override fun addSpec(spec: CompatUISpec) { + if (findSpec(spec.name) != null) { + throw IllegalStateException("Spec with name:${spec.name} already present") + } + allSpecs[spec.name] = spec + } + + override fun iterateOn(fn: (CompatUISpec) -> Unit) = + allSpecs.values.forEach(fn) + + override fun findSpec(name: String): CompatUISpec? = + allSpecs[name] +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUISpec.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUISpec.kt new file mode 100644 index 000000000000..0912bf115666 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/FakeCompatUISpec.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 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.compatui.impl + +import com.android.wm.shell.compatui.api.CompatUISpec + +/** + * Fake implementation for {@link ompatUISpec} + */ +class FakeCompatUISpec( + val name: String, + val lifecycle: FakeCompatUILifecyclePredicates = FakeCompatUILifecyclePredicates(), + val layout: FakeCompatUILayout +) { + fun getSpec(): CompatUISpec = CompatUISpec( + name = name, + log = {str -> android.util.Log.d("COMPAT_UI_TEST", str)}, + lifecycle = lifecycle.getLifecycle(), + layout = layout.getLayout() + ) +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt new file mode 100644 index 000000000000..628c9cdd9339 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2024 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.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.content.pm.ActivityInfo +import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE +import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT +import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT +import android.graphics.Rect +import android.os.Binder +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.testing.AndroidTestingRunner +import android.view.Display.DEFAULT_DISPLAY +import android.window.WindowContainerTransaction +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.dx.mockito.inline.extended.ExtendedMockito.never +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE +import com.android.window.flags.Flags.FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.common.TaskStackListenerImpl +import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask +import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask +import com.android.wm.shell.desktopmode.persistence.Desktop +import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.Transitions +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertTrue +import kotlin.test.assertNotNull +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.isNull +import org.mockito.Mock +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.capture +import org.mockito.kotlin.eq +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +/** + * Test class for {@link DesktopActivityOrientationChangeHandler} + * + * Usage: atest WMShellUnitTests:DesktopActivityOrientationChangeHandlerTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +@ExperimentalCoroutinesApi +@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE, FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE) +class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { + @JvmField @Rule val setFlagsRule = SetFlagsRule() + + @Mock lateinit var testExecutor: ShellExecutor + @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer + @Mock lateinit var transitions: Transitions + @Mock lateinit var resizeTransitionHandler: ToggleResizeDesktopTaskTransitionHandler + @Mock lateinit var taskStackListener: TaskStackListenerImpl + @Mock lateinit var persistentRepository: DesktopPersistentRepository + + private lateinit var mockitoSession: StaticMockitoSession + private lateinit var handler: DesktopActivityOrientationChangeHandler + private lateinit var shellInit: ShellInit + private lateinit var taskRepository: DesktopModeTaskRepository + private lateinit var testScope: CoroutineScope + // Mock running tasks are registered here so we can get the list from mock shell task organizer. + private val runningTasks = mutableListOf<RunningTaskInfo>() + + @Before + fun setUp() { + Dispatchers.setMain(StandardTestDispatcher()) + mockitoSession = + mockitoSession() + .strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java) + .startMocking() + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + + testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + shellInit = spy(ShellInit(testExecutor)) + taskRepository = + DesktopModeTaskRepository(context, shellInit, persistentRepository, testScope) + whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } + whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } + whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn( + Desktop.getDefaultInstance() + ) + + handler = DesktopActivityOrientationChangeHandler(context, shellInit, shellTaskOrganizer, + taskStackListener, resizeTransitionHandler, taskRepository) + + shellInit.init() + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + + runningTasks.clear() + testScope.cancel() + } + + @Test + fun instantiate_addInitCallback() { + verify(shellInit).addInitCallback(any(), any<DesktopActivityOrientationChangeHandler>()) + } + + @Test + fun instantiate_cannotEnterDesktopMode_doNotAddInitCallback() { + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false) + clearInvocations(shellInit) + + handler = DesktopActivityOrientationChangeHandler(context, shellInit, shellTaskOrganizer, + taskStackListener, resizeTransitionHandler, taskRepository) + + verify(shellInit, never()).addInitCallback(any(), + any<DesktopActivityOrientationChangeHandler>()) + } + + @Test + fun handleActivityOrientationChange_resizeable_doNothing() { + val task = setUpFreeformTask() + + taskStackListener.onActivityRequestedOrientationChanged(task.taskId, + SCREEN_ORIENTATION_LANDSCAPE) + + verify(resizeTransitionHandler, never()).startTransition(any(), any()) + } + + @Test + fun handleActivityOrientationChange_nonResizeableFullscreen_doNothing() { + val task = createFullscreenTask() + task.isResizeable = false + val activityInfo = ActivityInfo() + activityInfo.screenOrientation = SCREEN_ORIENTATION_PORTRAIT + task.topActivityInfo = activityInfo + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + taskRepository.addActiveTask(DEFAULT_DISPLAY, task.taskId) + taskRepository.updateTaskVisibility(DEFAULT_DISPLAY, task.taskId, visible = true) + runningTasks.add(task) + + taskStackListener.onActivityRequestedOrientationChanged(task.taskId, + SCREEN_ORIENTATION_LANDSCAPE) + + verify(resizeTransitionHandler, never()).startTransition(any(), any()) + } + + @Test + fun handleActivityOrientationChange_nonResizeablePortrait_requestSameOrientation_doNothing() { + val task = setUpFreeformTask(isResizeable = false) + val newTask = setUpFreeformTask(isResizeable = false, + orientation = SCREEN_ORIENTATION_SENSOR_PORTRAIT) + + handler.handleActivityOrientationChange(task, newTask) + + verify(resizeTransitionHandler, never()).startTransition(any(), any()) + } + + @Test + fun handleActivityOrientationChange_notInDesktopMode_doNothing() { + val task = setUpFreeformTask(isResizeable = false) + taskRepository.updateTaskVisibility(task.displayId, task.taskId, visible = false) + + taskStackListener.onActivityRequestedOrientationChanged(task.taskId, + SCREEN_ORIENTATION_LANDSCAPE) + + verify(resizeTransitionHandler, never()).startTransition(any(), any()) + } + + @Test + fun handleActivityOrientationChange_nonResizeablePortrait_respectLandscapeRequest() { + val task = setUpFreeformTask(isResizeable = false) + val oldBounds = task.configuration.windowConfiguration.bounds + val newTask = setUpFreeformTask(isResizeable = false, + orientation = SCREEN_ORIENTATION_LANDSCAPE) + + handler.handleActivityOrientationChange(task, newTask) + + val wct = getLatestResizeDesktopTaskWct() + val finalBounds = findBoundsChange(wct, newTask) + assertNotNull(finalBounds) + val finalWidth = finalBounds.width() + val finalHeight = finalBounds.height() + // Bounds is landscape. + assertTrue(finalWidth > finalHeight) + // Aspect ratio remains the same. + assertEquals(oldBounds.height() / oldBounds.width(), finalWidth / finalHeight) + // Anchor point for resizing is at the center. + assertEquals(oldBounds.centerX(), finalBounds.centerX()) + } + + @Test + fun handleActivityOrientationChange_nonResizeableLandscape_respectPortraitRequest() { + val oldBounds = Rect(0, 0, 500, 200) + val task = setUpFreeformTask( + isResizeable = false, orientation = SCREEN_ORIENTATION_LANDSCAPE, bounds = oldBounds + ) + val newTask = setUpFreeformTask(isResizeable = false, bounds = oldBounds) + + handler.handleActivityOrientationChange(task, newTask) + + val wct = getLatestResizeDesktopTaskWct() + val finalBounds = findBoundsChange(wct, newTask) + assertNotNull(finalBounds) + val finalWidth = finalBounds.width() + val finalHeight = finalBounds.height() + // Bounds is portrait. + assertTrue(finalHeight > finalWidth) + // Aspect ratio remains the same. + assertEquals(oldBounds.width() / oldBounds.height(), finalHeight / finalWidth) + // Anchor point for resizing is at the center. + assertEquals(oldBounds.centerX(), finalBounds.centerX()) + } + + private fun setUpFreeformTask( + displayId: Int = DEFAULT_DISPLAY, + isResizeable: Boolean = true, + orientation: Int = SCREEN_ORIENTATION_PORTRAIT, + bounds: Rect? = Rect(0, 0, 200, 500) + ): RunningTaskInfo { + val task = createFreeformTask(displayId, bounds) + val activityInfo = ActivityInfo() + activityInfo.screenOrientation = orientation + task.topActivityInfo = activityInfo + task.isResizeable = isResizeable + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + taskRepository.addActiveTask(displayId, task.taskId) + taskRepository.updateTaskVisibility(displayId, task.taskId, visible = true) + taskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId) + runningTasks.add(task) + return task + } + + private fun getLatestResizeDesktopTaskWct( + currentBounds: Rect? = null + ): WindowContainerTransaction { + val arg: ArgumentCaptor<WindowContainerTransaction> = + ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(resizeTransitionHandler, atLeastOnce()) + .startTransition(capture(arg), eq(currentBounds)) + return arg.value + } + + private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? = + wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt index 4548fcb06c55..ca972296e8d4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt @@ -16,14 +16,17 @@ package com.android.wm.shell.desktopmode -import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.dx.mockito.inline.extended.ExtendedMockito.verify import com.android.internal.util.FrameworkStatsLog import com.android.modules.utils.testing.ExtendedMockitoRule import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.TaskUpdate +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UnminimizeReason +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UNSET_MINIMIZE_REASON +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UNSET_UNMINIMIZE_REASON import kotlinx.coroutines.runBlocking -import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.kotlin.eq @@ -33,7 +36,7 @@ import org.mockito.kotlin.eq */ class DesktopModeEventLoggerTest { - private val desktopModeEventLogger = DesktopModeEventLogger() + private val desktopModeEventLogger = DesktopModeEventLogger() @JvmField @Rule @@ -44,7 +47,7 @@ class DesktopModeEventLoggerTest { fun logSessionEnter_enterReason() = runBlocking { desktopModeEventLogger.logSessionEnter(sessionId = SESSION_ID, EnterReason.UNKNOWN_ENTER) - ExtendedMockito.verify { + verify { FrameworkStatsLog.write( eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), /* event */ @@ -63,7 +66,7 @@ class DesktopModeEventLoggerTest { fun logSessionExit_exitReason() = runBlocking { desktopModeEventLogger.logSessionExit(sessionId = SESSION_ID, ExitReason.UNKNOWN_EXIT) - ExtendedMockito.verify { + verify { FrameworkStatsLog.write( eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), /* event */ @@ -82,7 +85,7 @@ class DesktopModeEventLoggerTest { fun logTaskAdded_taskUpdate() = runBlocking { desktopModeEventLogger.logTaskAdded(sessionId = SESSION_ID, TASK_UPDATE) - ExtendedMockito.verify { + verify { FrameworkStatsLog.write(eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), /* task_event */ eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED), @@ -99,7 +102,11 @@ class DesktopModeEventLoggerTest { /* task_y */ eq(TASK_UPDATE.taskY), /* session_id */ - eq(SESSION_ID)) + eq(SESSION_ID), + eq(UNSET_MINIMIZE_REASON), + eq(UNSET_UNMINIMIZE_REASON), + /* visible_task_count */ + eq(TASK_COUNT)) } } @@ -107,7 +114,7 @@ class DesktopModeEventLoggerTest { fun logTaskRemoved_taskUpdate() = runBlocking { desktopModeEventLogger.logTaskRemoved(sessionId = SESSION_ID, TASK_UPDATE) - ExtendedMockito.verify { + verify { FrameworkStatsLog.write(eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), /* task_event */ eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED), @@ -124,7 +131,11 @@ class DesktopModeEventLoggerTest { /* task_y */ eq(TASK_UPDATE.taskY), /* session_id */ - eq(SESSION_ID)) + eq(SESSION_ID), + eq(UNSET_MINIMIZE_REASON), + eq(UNSET_UNMINIMIZE_REASON), + /* visible_task_count */ + eq(TASK_COUNT)) } } @@ -132,10 +143,11 @@ class DesktopModeEventLoggerTest { fun logTaskInfoChanged_taskUpdate() = runBlocking { desktopModeEventLogger.logTaskInfoChanged(sessionId = SESSION_ID, TASK_UPDATE) - ExtendedMockito.verify { + verify { FrameworkStatsLog.write(eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), /* task_event */ - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED), + eq(FrameworkStatsLog + .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED), /* instance_id */ eq(TASK_UPDATE.instanceId), /* uid */ @@ -149,7 +161,77 @@ class DesktopModeEventLoggerTest { /* task_y */ eq(TASK_UPDATE.taskY), /* session_id */ - eq(SESSION_ID)) + eq(SESSION_ID), + eq(UNSET_MINIMIZE_REASON), + eq(UNSET_UNMINIMIZE_REASON), + /* visible_task_count */ + eq(TASK_COUNT)) + } + } + + @Test + fun logTaskInfoChanged_logsTaskUpdateWithMinimizeReason() = runBlocking { + desktopModeEventLogger.logTaskInfoChanged(sessionId = SESSION_ID, + createTaskUpdate(minimizeReason = MinimizeReason.TASK_LIMIT)) + + verify { + FrameworkStatsLog.write(eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), + /* task_event */ + eq(FrameworkStatsLog + .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED), + /* instance_id */ + eq(TASK_UPDATE.instanceId), + /* uid */ + eq(TASK_UPDATE.uid), + /* task_height */ + eq(TASK_UPDATE.taskHeight), + /* task_width */ + eq(TASK_UPDATE.taskWidth), + /* task_x */ + eq(TASK_UPDATE.taskX), + /* task_y */ + eq(TASK_UPDATE.taskY), + /* session_id */ + eq(SESSION_ID), + /* minimize_reason */ + eq(MinimizeReason.TASK_LIMIT.reason), + /* unminimize_reason */ + eq(UNSET_UNMINIMIZE_REASON), + /* visible_task_count */ + eq(TASK_COUNT)) + } + } + + @Test + fun logTaskInfoChanged_logsTaskUpdateWithUnminimizeReason() = runBlocking { + desktopModeEventLogger.logTaskInfoChanged(sessionId = SESSION_ID, + createTaskUpdate(unminimizeReason = UnminimizeReason.TASKBAR_TAP)) + + verify { + FrameworkStatsLog.write(eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), + /* task_event */ + eq(FrameworkStatsLog + .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED), + /* instance_id */ + eq(TASK_UPDATE.instanceId), + /* uid */ + eq(TASK_UPDATE.uid), + /* task_height */ + eq(TASK_UPDATE.taskHeight), + /* task_width */ + eq(TASK_UPDATE.taskWidth), + /* task_x */ + eq(TASK_UPDATE.taskX), + /* task_y */ + eq(TASK_UPDATE.taskY), + /* session_id */ + eq(SESSION_ID), + /* minimize_reason */ + eq(UNSET_MINIMIZE_REASON), + /* unminimize_reason */ + eq(UnminimizeReason.TASKBAR_TAP.reason), + /* visible_task_count */ + eq(TASK_COUNT)) } } @@ -161,9 +243,17 @@ class DesktopModeEventLoggerTest { private const val TASK_Y = 0 private const val TASK_HEIGHT = 100 private const val TASK_WIDTH = 100 + private const val TASK_COUNT = 1 private val TASK_UPDATE = TaskUpdate( - TASK_ID, TASK_UID, TASK_HEIGHT, TASK_WIDTH, TASK_X, TASK_Y + TASK_ID, TASK_UID, TASK_HEIGHT, TASK_WIDTH, TASK_X, TASK_Y, + visibleTaskCount = TASK_COUNT, ) + + private fun createTaskUpdate( + minimizeReason: MinimizeReason? = null, + unminimizeReason: UnminimizeReason? = null, + ) = TaskUpdate(TASK_ID, TASK_UID, TASK_HEIGHT, TASK_WIDTH, TASK_X, TASK_Y, minimizeReason, + unminimizeReason, TASK_COUNT) } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt index fb03f20f939c..d399b20abb2a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt @@ -19,7 +19,11 @@ import android.app.ActivityManager import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.content.Context +import android.graphics.Point +import android.graphics.Rect import android.os.IBinder +import android.os.SystemProperties +import android.os.Trace import android.testing.AndroidTestingRunner import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE @@ -36,11 +40,13 @@ import android.window.TransitionInfo import android.window.TransitionInfo.Change import android.window.WindowContainerToken import androidx.test.filters.SmallTest -import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn +import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.modules.utils.testing.ExtendedMockitoRule +import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.TaskUpdate import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT @@ -49,26 +55,27 @@ import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_ import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN -import com.android.wm.shell.shared.DesktopModeStatus +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.TransitionInfoBuilder import com.android.wm.shell.transition.Transitions -import com.google.common.truth.Truth.assertThat +import junit.framework.Assert.assertNotNull +import junit.framework.Assert.assertNull import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor -import org.mockito.Mock -import org.mockito.Mockito -import org.mockito.Mockito.mock -import org.mockito.Mockito.verify import org.mockito.kotlin.any import org.mockito.kotlin.eq +import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.same +import org.mockito.kotlin.spy import org.mockito.kotlin.times +import org.mockito.kotlin.verify import org.mockito.kotlin.verifyZeroInteractions +import org.mockito.kotlin.whenever /** * Test class for {@link DesktopModeLoggerTransitionObserver} @@ -77,20 +84,21 @@ import org.mockito.kotlin.verifyZeroInteractions */ @SmallTest @RunWith(AndroidTestingRunner::class) -class DesktopModeLoggerTransitionObserverTest { +class DesktopModeLoggerTransitionObserverTest : ShellTestCase() { @JvmField @Rule val extendedMockitoRule = ExtendedMockitoRule.Builder(this) - .mockStatic(DesktopModeEventLogger::class.java) .mockStatic(DesktopModeStatus::class.java) + .mockStatic(SystemProperties::class.java) + .mockStatic(Trace::class.java) .build()!! - @Mock lateinit var testExecutor: ShellExecutor - @Mock private lateinit var mockShellInit: ShellInit - @Mock private lateinit var transitions: Transitions - @Mock private lateinit var context: Context + private val testExecutor = mock<ShellExecutor>() + private val mockShellInit = mock<ShellInit>() + private val transitions = mock<Transitions>() + private val context = mock<Context>() private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver private lateinit var shellInit: ShellInit @@ -98,9 +106,9 @@ class DesktopModeLoggerTransitionObserverTest { @Before fun setup() { - doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) } - shellInit = Mockito.spy(ShellInit(testExecutor)) - desktopModeEventLogger = mock(DesktopModeEventLogger::class.java) + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + shellInit = spy(ShellInit(testExecutor)) + desktopModeEventLogger = mock<DesktopModeEventLogger>() transitionObserver = DesktopModeLoggerTransitionObserver( @@ -121,7 +129,7 @@ class DesktopModeLoggerTransitionObserverTest { @Test fun transitOpen_notFreeformWindow_doesNotLogTaskAddedOrSessionEnter() { - val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)) + val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() callOnTransitionReady(transitionInfo) @@ -132,22 +140,18 @@ class DesktopModeLoggerTransitionObserverTest { @Test fun transitOpen_logTaskAddedAndEnterReasonAppFreeformIntent() { - val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FREEFORM)) val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() callOnTransitionReady(transitionInfo) - val sessionId = transitionObserver.getLoggerSessionId() - assertThat(sessionId).isNotNull() - verify(desktopModeEventLogger, times(1)) - .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_FREEFORM_INTENT)) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) - verifyZeroInteractions(desktopModeEventLogger) + verifyTaskAddedAndEnterLogging(EnterReason.APP_FREEFORM_INTENT, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) } @Test fun transitEndDragToDesktop_logTaskAddedAndEnterReasonAppHandleDrag() { - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) // task change is finalised when drag ends val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0) @@ -155,82 +159,62 @@ class DesktopModeLoggerTransitionObserverTest { .build() callOnTransitionReady(transitionInfo) - val sessionId = transitionObserver.getLoggerSessionId() - assertThat(sessionId).isNotNull() - verify(desktopModeEventLogger, times(1)) - .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_HANDLE_DRAG)) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) - verifyZeroInteractions(desktopModeEventLogger) + verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_DRAG, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) } @Test fun transitEnterDesktopByButtonTap_logTaskAddedAndEnterReasonButtonTap() { - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) val transitionInfo = TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON, 0) .addChange(change) .build() callOnTransitionReady(transitionInfo) - val sessionId = transitionObserver.getLoggerSessionId() - assertThat(sessionId).isNotNull() - verify(desktopModeEventLogger, times(1)) - .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_HANDLE_MENU_BUTTON)) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) - verifyZeroInteractions(desktopModeEventLogger) + verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_MENU_BUTTON, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) } @Test fun transitEnterDesktopFromAppFromOverview_logTaskAddedAndEnterReasonAppFromOverview() { - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) val transitionInfo = TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0) .addChange(change) .build() callOnTransitionReady(transitionInfo) - val sessionId = transitionObserver.getLoggerSessionId() - assertThat(sessionId).isNotNull() - verify(desktopModeEventLogger, times(1)) - .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_FROM_OVERVIEW)) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) - verifyZeroInteractions(desktopModeEventLogger) + verifyTaskAddedAndEnterLogging(EnterReason.APP_FROM_OVERVIEW, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) } @Test fun transitEnterDesktopFromKeyboardShortcut_logTaskAddedAndEnterReasonKeyboardShortcut() { - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) val transitionInfo = TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT, 0) .addChange(change) .build() callOnTransitionReady(transitionInfo) - val sessionId = transitionObserver.getLoggerSessionId() - assertThat(sessionId).isNotNull() - verify(desktopModeEventLogger, times(1)) - .logSessionEnter(eq(sessionId!!), eq(EnterReason.KEYBOARD_SHORTCUT_ENTER)) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) - verifyZeroInteractions(desktopModeEventLogger) + verifyTaskAddedAndEnterLogging(EnterReason.KEYBOARD_SHORTCUT_ENTER, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) } @Test fun transitToFront_logTaskAddedAndEnterReasonOverview() { - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, 0).addChange(change).build() callOnTransitionReady(transitionInfo) - val sessionId = transitionObserver.getLoggerSessionId() - assertThat(sessionId).isNotNull() - verify(desktopModeEventLogger, times(1)) - .logSessionEnter(eq(sessionId!!), eq(EnterReason.OVERVIEW)) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) - verifyZeroInteractions(desktopModeEventLogger) + verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) } @Test @@ -238,35 +222,30 @@ class DesktopModeLoggerTransitionObserverTest { // previous exit to overview transition val previousSessionId = 1 // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(previousTaskInfo) transitionObserver.setLoggerSessionId(previousSessionId) - val previousChange = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) val previousTransitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) - .addChange(previousChange) + .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo)) .build() callOnTransitionReady(previousTransitionInfo) - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(previousSessionId), any()) - verify(desktopModeEventLogger, times(1)) - .logSessionExit(eq(previousSessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW)) + verifyTaskRemovedAndExitLogging( + previousSessionId, ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE) // Enter desktop mode from cancelled recents has no transition. Enter is detected on the // next transition involving freeform windows // TRANSIT_TO_FRONT - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, 0).addChange(change).build() callOnTransitionReady(transitionInfo) - val sessionId = transitionObserver.getLoggerSessionId() - assertThat(sessionId).isNotNull() - verify(desktopModeEventLogger, times(1)) - .logSessionEnter(eq(sessionId!!), eq(EnterReason.OVERVIEW)) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) - verifyZeroInteractions(desktopModeEventLogger) + verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) } @Test @@ -274,35 +253,30 @@ class DesktopModeLoggerTransitionObserverTest { // previous exit to overview transition val previousSessionId = 1 // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(previousTaskInfo) transitionObserver.setLoggerSessionId(previousSessionId) - val previousChange = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) val previousTransitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) - .addChange(previousChange) + .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo)) .build() callOnTransitionReady(previousTransitionInfo) - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(previousSessionId), any()) - verify(desktopModeEventLogger, times(1)) - .logSessionExit(eq(previousSessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW)) + verifyTaskRemovedAndExitLogging( + previousSessionId, ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE) // Enter desktop mode from cancelled recents has no transition. Enter is detected on the // next transition involving freeform windows // TRANSIT_CHANGE - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) val transitionInfo = TransitionInfoBuilder(TRANSIT_CHANGE, 0).addChange(change).build() callOnTransitionReady(transitionInfo) - val sessionId = transitionObserver.getLoggerSessionId() - assertThat(sessionId).isNotNull() - verify(desktopModeEventLogger, times(1)) - .logSessionEnter(eq(sessionId!!), eq(EnterReason.OVERVIEW)) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) - verifyZeroInteractions(desktopModeEventLogger) + verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) } @Test @@ -310,35 +284,30 @@ class DesktopModeLoggerTransitionObserverTest { // previous exit to overview transition val previousSessionId = 1 // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(previousTaskInfo) transitionObserver.setLoggerSessionId(previousSessionId) - val previousChange = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) val previousTransitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) - .addChange(previousChange) + .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo)) .build() callOnTransitionReady(previousTransitionInfo) - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(previousSessionId), any()) - verify(desktopModeEventLogger, times(1)) - .logSessionExit(eq(previousSessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW)) + verifyTaskRemovedAndExitLogging( + previousSessionId, ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE) // Enter desktop mode from cancelled recents has no transition. Enter is detected on the // next transition involving freeform windows // TRANSIT_OPEN - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() callOnTransitionReady(transitionInfo) - val sessionId = transitionObserver.getLoggerSessionId() - assertThat(sessionId).isNotNull() - verify(desktopModeEventLogger, times(1)) - .logSessionEnter(eq(sessionId!!), eq(EnterReason.OVERVIEW)) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) - verifyZeroInteractions(desktopModeEventLogger) + verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) } @Test @@ -349,286 +318,456 @@ class DesktopModeLoggerTransitionObserverTest { // previous exit to overview transition val previousSessionId = 1 // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(previousTaskInfo) transitionObserver.setLoggerSessionId(previousSessionId) - val previousChange = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) val previousTransitionInfo = - TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) - .addChange(previousChange) - .build() + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) + .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo)) + .build() callOnTransitionReady(previousTransitionInfo) - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(previousSessionId), any()) - verify(desktopModeEventLogger, times(1)) - .logSessionExit(eq(previousSessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW)) + verifyTaskRemovedAndExitLogging( + previousSessionId, ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE) // TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) val transitionInfo = - TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0) - .addChange(change) - .build() + TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0) + .addChange(change) + .build() callOnTransitionReady(transitionInfo) - val sessionId = transitionObserver.getLoggerSessionId() - assertThat(sessionId).isNotNull() - verify(desktopModeEventLogger, times(1)) - .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_FROM_OVERVIEW)) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) - verifyZeroInteractions(desktopModeEventLogger) + verifyTaskAddedAndEnterLogging(EnterReason.APP_FROM_OVERVIEW, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) } @Test fun transitEnterDesktopFromUnknown_logTaskAddedAndEnterReasonUnknown() { - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) val transitionInfo = TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN, 0).addChange(change).build() callOnTransitionReady(transitionInfo) - val sessionId = transitionObserver.getLoggerSessionId() - assertThat(sessionId).isNotNull() - verify(desktopModeEventLogger, times(1)) - .logSessionEnter(eq(sessionId!!), eq(EnterReason.UNKNOWN_ENTER)) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) - verifyZeroInteractions(desktopModeEventLogger) + verifyTaskAddedAndEnterLogging(EnterReason.UNKNOWN_ENTER, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) } @Test fun transitWake_logTaskAddedAndEnterReasonScreenOn() { - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) val transitionInfo = TransitionInfoBuilder(TRANSIT_WAKE, 0).addChange(change).build() callOnTransitionReady(transitionInfo) - val sessionId = transitionObserver.getLoggerSessionId() - assertThat(sessionId).isNotNull() - verify(desktopModeEventLogger, times(1)) - .logSessionEnter(eq(sessionId!!), eq(EnterReason.SCREEN_ON)) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) - verifyZeroInteractions(desktopModeEventLogger) + verifyTaskAddedAndEnterLogging(EnterReason.SCREEN_ON, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) + } + + @Test + fun transitBack_previousExitReasonScreenOff_logTaskAddedAndEnterReasonScreenOn() { + val freeformTask = createTaskInfo(WINDOWING_MODE_FREEFORM) + // Previous Exit reason recorded as Screen Off + val sessionId = 1 + transitionObserver.addTaskInfosToCachedMap(freeformTask) + transitionObserver.setLoggerSessionId(sessionId) + callOnTransitionReady(TransitionInfoBuilder(TRANSIT_SLEEP).build()) + verifyTaskRemovedAndExitLogging(sessionId, ExitReason.SCREEN_OFF, DEFAULT_TASK_UPDATE) + // Enter desktop through back transition, this happens when user enters after dismissing + // keyguard + val change = createChange(TRANSIT_TO_FRONT, freeformTask) + val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_BACK, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging(EnterReason.SCREEN_ON, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) } @Test - fun transitSleep_logTaskAddedAndExitReasonScreenOff_sessionIdNull() { + fun transitEndDragToDesktop_previousExitReasonScreenOff_logTaskAddedAndEnterReasonAppDrag() { + val freeformTask = createTaskInfo(WINDOWING_MODE_FREEFORM) + // Previous Exit reason recorded as Screen Off + val sessionId = 1 + transitionObserver.addTaskInfosToCachedMap(freeformTask) + transitionObserver.setLoggerSessionId(sessionId) + callOnTransitionReady(TransitionInfoBuilder(TRANSIT_SLEEP).build()) + verifyTaskRemovedAndExitLogging(sessionId, ExitReason.SCREEN_OFF, DEFAULT_TASK_UPDATE) + + // Enter desktop through app handle drag. This represents cases where instead of moving to + // desktop right after turning the screen on, we move to fullscreen then move another task + // to desktop + val transitionInfo = + TransitionInfoBuilder(Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0) + .addChange(createChange(TRANSIT_TO_FRONT, freeformTask)) + .build() + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_DRAG, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) + } + + @Test + fun transitSleep_logTaskRemovedAndExitReasonScreenOff_sessionIdNull() { val sessionId = 1 // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) transitionObserver.setLoggerSessionId(sessionId) val transitionInfo = TransitionInfoBuilder(TRANSIT_SLEEP).build() callOnTransitionReady(transitionInfo) - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) - verify(desktopModeEventLogger, times(1)) - .logSessionExit(eq(sessionId), eq(ExitReason.SCREEN_OFF)) - verifyZeroInteractions(desktopModeEventLogger) - assertThat(transitionObserver.getLoggerSessionId()).isNull() + verifyTaskRemovedAndExitLogging(sessionId, ExitReason.SCREEN_OFF, DEFAULT_TASK_UPDATE) } @Test fun transitExitDesktopTaskDrag_logTaskRemovedAndExitReasonDragToExit_sessionIdNull() { val sessionId = 1 // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) transitionObserver.setLoggerSessionId(sessionId) // window mode changing from FREEFORM to FULLSCREEN - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)) + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) val transitionInfo = TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG).addChange(change).build() callOnTransitionReady(transitionInfo) - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) - verify(desktopModeEventLogger, times(1)) - .logSessionExit(eq(sessionId), eq(ExitReason.DRAG_TO_EXIT)) - verifyZeroInteractions(desktopModeEventLogger) - assertThat(transitionObserver.getLoggerSessionId()).isNull() + verifyTaskRemovedAndExitLogging(sessionId, ExitReason.DRAG_TO_EXIT, DEFAULT_TASK_UPDATE) } @Test fun transitExitDesktopAppHandleButton_logTaskRemovedAndExitReasonButton_sessionIdNull() { val sessionId = 1 // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) transitionObserver.setLoggerSessionId(sessionId) // window mode changing from FREEFORM to FULLSCREEN - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)) + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) val transitionInfo = TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON) .addChange(change) .build() callOnTransitionReady(transitionInfo) - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) - verify(desktopModeEventLogger, times(1)) - .logSessionExit(eq(sessionId), eq(ExitReason.APP_HANDLE_MENU_BUTTON_EXIT)) - verifyZeroInteractions(desktopModeEventLogger) - assertThat(transitionObserver.getLoggerSessionId()).isNull() + verifyTaskRemovedAndExitLogging( + sessionId, ExitReason.APP_HANDLE_MENU_BUTTON_EXIT, DEFAULT_TASK_UPDATE) } @Test fun transitExitDesktopUsingKeyboard_logTaskRemovedAndExitReasonKeyboard_sessionIdNull() { val sessionId = 1 // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) transitionObserver.setLoggerSessionId(sessionId) // window mode changing from FREEFORM to FULLSCREEN - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)) + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) val transitionInfo = TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT).addChange(change).build() callOnTransitionReady(transitionInfo) - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) - verify(desktopModeEventLogger, times(1)) - .logSessionExit(eq(sessionId), eq(ExitReason.KEYBOARD_SHORTCUT_EXIT)) - verifyZeroInteractions(desktopModeEventLogger) - assertThat(transitionObserver.getLoggerSessionId()).isNull() + verifyTaskRemovedAndExitLogging( + sessionId, ExitReason.KEYBOARD_SHORTCUT_EXIT, DEFAULT_TASK_UPDATE) } @Test fun transitExitDesktopUnknown_logTaskRemovedAndExitReasonUnknown_sessionIdNull() { val sessionId = 1 // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) transitionObserver.setLoggerSessionId(sessionId) // window mode changing from FREEFORM to FULLSCREEN - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)) + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) val transitionInfo = TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN).addChange(change).build() callOnTransitionReady(transitionInfo) - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) - verify(desktopModeEventLogger, times(1)) - .logSessionExit(eq(sessionId), eq(ExitReason.UNKNOWN_EXIT)) - verifyZeroInteractions(desktopModeEventLogger) - assertThat(transitionObserver.getLoggerSessionId()).isNull() + verifyTaskRemovedAndExitLogging(sessionId, ExitReason.UNKNOWN_EXIT, DEFAULT_TASK_UPDATE) } @Test fun transitToFrontWithFlagRecents_logTaskRemovedAndExitReasonOverview_sessionIdNull() { val sessionId = 1 // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) transitionObserver.setLoggerSessionId(sessionId) // recents transition - val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val change = createChange(TRANSIT_TO_BACK, createTaskInfo(WINDOWING_MODE_FREEFORM)) val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change).build() callOnTransitionReady(transitionInfo) - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) - verify(desktopModeEventLogger, times(1)) - .logSessionExit(eq(sessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW)) - verifyZeroInteractions(desktopModeEventLogger) - assertThat(transitionObserver.getLoggerSessionId()).isNull() + verifyTaskRemovedAndExitLogging( + sessionId, ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE) } @Test fun transitClose_logTaskRemovedAndExitReasonTaskFinished_sessionIdNull() { val sessionId = 1 // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) transitionObserver.setLoggerSessionId(sessionId) // task closing - val change = createChange(TRANSIT_CLOSE, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)) + val change = createChange(TRANSIT_CLOSE, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE).addChange(change).build() callOnTransitionReady(transitionInfo) - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) - verify(desktopModeEventLogger, times(1)) - .logSessionExit(eq(sessionId), eq(ExitReason.TASK_FINISHED)) - verifyZeroInteractions(desktopModeEventLogger) - assertThat(transitionObserver.getLoggerSessionId()).isNull() + verifyTaskRemovedAndExitLogging(sessionId, ExitReason.TASK_FINISHED, DEFAULT_TASK_UPDATE) } @Test fun sessionExitByRecents_cancelledAnimation_sessionRestored() { val sessionId = 1 // add a freeform task to an existing session - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(taskInfo) transitionObserver.setLoggerSessionId(sessionId) // recents transition sent freeform window to back - val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + val change = createChange(TRANSIT_TO_BACK, taskInfo) val transitionInfo1 = TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change).build() callOnTransitionReady(transitionInfo1) - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) - verify(desktopModeEventLogger, times(1)) - .logSessionExit(eq(sessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW)) - assertThat(transitionObserver.getLoggerSessionId()).isNull() + + verifyTaskRemovedAndExitLogging( + sessionId, ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE) val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build() callOnTransitionReady(transitionInfo2) - verify(desktopModeEventLogger, times(1)).logSessionEnter(any(), any()) - verify(desktopModeEventLogger, times(1)).logTaskAdded(any(), any()) + verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) } @Test fun sessionAlreadyStarted_newFreeformTaskAdded_logsTaskAdded() { val sessionId = 1 // add an existing freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) transitionObserver.setLoggerSessionId(sessionId) // new freeform task added - val change = createChange(TRANSIT_OPEN, createTaskInfo(2, WINDOWING_MODE_FREEFORM)) + val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2)) val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() callOnTransitionReady(transitionInfo) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any()) + verify(desktopModeEventLogger, times(1)) + .logTaskAdded(eq(sessionId), + eq(DEFAULT_TASK_UPDATE.copy(instanceId = 2, visibleTaskCount = 2))) verify(desktopModeEventLogger, never()).logSessionEnter(any(), any()) } @Test + fun sessionAlreadyStarted_taskPositionChanged_logsTaskUpdate() { + val sessionId = 1 + // add an existing freeform task + val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(taskInfo) + transitionObserver.setLoggerSessionId(sessionId) + + // task position changed + val newTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM, taskX = DEFAULT_TASK_X + 100) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_CHANGE, 0) + .addChange(createChange(TRANSIT_CHANGE, newTaskInfo)) + .build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)) + .logTaskInfoChanged( + eq(sessionId), + eq(DEFAULT_TASK_UPDATE.copy(taskX = DEFAULT_TASK_X + 100, visibleTaskCount = 1))) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun sessionAlreadyStarted_taskResized_logsTaskUpdate() { + val sessionId = 1 + // add an existing freeform task + val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(taskInfo) + transitionObserver.setLoggerSessionId(sessionId) + + // task resized + val newTaskInfo = + createTaskInfo( + WINDOWING_MODE_FREEFORM, + taskWidth = DEFAULT_TASK_WIDTH + 100, + taskHeight = DEFAULT_TASK_HEIGHT - 100) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_CHANGE, 0) + .addChange(createChange(TRANSIT_CHANGE, newTaskInfo)) + .build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)) + .logTaskInfoChanged( + eq(sessionId), + eq( + DEFAULT_TASK_UPDATE.copy( + taskWidth = DEFAULT_TASK_WIDTH + 100, taskHeight = DEFAULT_TASK_HEIGHT - 100, + visibleTaskCount = 1))) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun sessionAlreadyStarted_multipleTasksUpdated_logsTaskUpdateForCorrectTask() { + val sessionId = 1 + // add 2 existing freeform task + val taskInfo1 = createTaskInfo(WINDOWING_MODE_FREEFORM) + val taskInfo2 = createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2) + transitionObserver.addTaskInfosToCachedMap(taskInfo1) + transitionObserver.addTaskInfosToCachedMap(taskInfo2) + transitionObserver.setLoggerSessionId(sessionId) + + // task 1 position update + val newTaskInfo1 = createTaskInfo(WINDOWING_MODE_FREEFORM, taskX = DEFAULT_TASK_X + 100) + val transitionInfo1 = + TransitionInfoBuilder(TRANSIT_CHANGE, 0) + .addChange(createChange(TRANSIT_CHANGE, newTaskInfo1)) + .build() + callOnTransitionReady(transitionInfo1) + + verify(desktopModeEventLogger, times(1)) + .logTaskInfoChanged( + eq(sessionId), eq(DEFAULT_TASK_UPDATE.copy( + taskX = DEFAULT_TASK_X + 100, visibleTaskCount = 2))) + verifyZeroInteractions(desktopModeEventLogger) + + // task 2 resize + val newTaskInfo2 = + createTaskInfo( + WINDOWING_MODE_FREEFORM, + id = 2, + taskWidth = DEFAULT_TASK_WIDTH + 100, + taskHeight = DEFAULT_TASK_HEIGHT - 100) + val transitionInfo2 = + TransitionInfoBuilder(TRANSIT_CHANGE, 0) + .addChange(createChange(TRANSIT_CHANGE, newTaskInfo2)) + .build() + + callOnTransitionReady(transitionInfo2) + + verify(desktopModeEventLogger, times(1)) + .logTaskInfoChanged( + eq(sessionId), + eq( + DEFAULT_TASK_UPDATE.copy( + instanceId = 2, + taskWidth = DEFAULT_TASK_WIDTH + 100, + taskHeight = DEFAULT_TASK_HEIGHT - 100, + visibleTaskCount = 2)), + ) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test fun sessionAlreadyStarted_freeformTaskRemoved_logsTaskRemoved() { val sessionId = 1 // add two existing freeform tasks - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM)) - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(2, WINDOWING_MODE_FREEFORM)) + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2)) transitionObserver.setLoggerSessionId(sessionId) - // new freeform task added - val change = createChange(TRANSIT_CLOSE, createTaskInfo(2, WINDOWING_MODE_FREEFORM)) + // new freeform task closed + val change = createChange(TRANSIT_CLOSE, createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2)) val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build() callOnTransitionReady(transitionInfo) - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any()) + verify(desktopModeEventLogger, times(1)) + .logTaskRemoved(eq(sessionId), eq(DEFAULT_TASK_UPDATE.copy( + instanceId = 2, visibleTaskCount = 1))) verify(desktopModeEventLogger, never()).logSessionExit(any(), any()) } /** Simulate calling the onTransitionReady() method */ private fun callOnTransitionReady(transitionInfo: TransitionInfo) { - val transition = mock(IBinder::class.java) - val startT = mock(SurfaceControl.Transaction::class.java) - val finishT = mock(SurfaceControl.Transaction::class.java) + val transition = mock<IBinder>() + val startT = mock<SurfaceControl.Transaction>() + val finishT = mock<SurfaceControl.Transaction>() transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT) } - companion object { - fun createTaskInfo(taskId: Int, windowMode: Int): ActivityManager.RunningTaskInfo { - val taskInfo = ActivityManager.RunningTaskInfo() - taskInfo.taskId = taskId - taskInfo.configuration.windowConfiguration.windowingMode = windowMode - - return taskInfo + private fun verifyTaskAddedAndEnterLogging(enterReason: EnterReason, taskUpdate: TaskUpdate) { + val sessionId = transitionObserver.getLoggerSessionId() + assertNotNull(sessionId) + verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), eq(enterReason)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), eq(taskUpdate)) + ExtendedMockito.verify { + Trace.setCounter( + eq(Trace.TRACE_TAG_WINDOW_MANAGER), + eq(DesktopModeLoggerTransitionObserver.VISIBLE_TASKS_COUNTER_NAME), + eq(taskUpdate.visibleTaskCount.toLong())) + } + ExtendedMockito.verify { + SystemProperties.set( + eq(DesktopModeLoggerTransitionObserver.VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY), + eq(taskUpdate.visibleTaskCount.toString())) } + verifyZeroInteractions(desktopModeEventLogger) + } + + private fun verifyTaskRemovedAndExitLogging( + sessionId: Int, + exitReason: ExitReason, + taskUpdate: TaskUpdate + ) { + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), eq(taskUpdate)) + verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId), eq(exitReason)) + verifyZeroInteractions(desktopModeEventLogger) + assertNull(transitionObserver.getLoggerSessionId()) + } + + private companion object { + const val DEFAULT_TASK_ID = 1 + const val DEFAULT_TASK_UID = 2 + const val DEFAULT_TASK_HEIGHT = 100 + const val DEFAULT_TASK_WIDTH = 200 + const val DEFAULT_TASK_X = 30 + const val DEFAULT_TASK_Y = 70 + const val DEFAULT_VISIBLE_TASK_COUNT = 0 + val DEFAULT_TASK_UPDATE = + TaskUpdate( + DEFAULT_TASK_ID, + DEFAULT_TASK_UID, + DEFAULT_TASK_HEIGHT, + DEFAULT_TASK_WIDTH, + DEFAULT_TASK_X, + DEFAULT_TASK_Y, + visibleTaskCount = DEFAULT_VISIBLE_TASK_COUNT, + ) + + fun createTaskInfo( + windowMode: Int, + id: Int = DEFAULT_TASK_ID, + uid: Int = DEFAULT_TASK_UID, + taskHeight: Int = DEFAULT_TASK_HEIGHT, + taskWidth: Int = DEFAULT_TASK_WIDTH, + taskX: Int = DEFAULT_TASK_X, + taskY: Int = DEFAULT_TASK_Y, + ) = + ActivityManager.RunningTaskInfo().apply { + taskId = id + effectiveUid = uid + configuration.windowConfiguration.apply { + windowingMode = windowMode + positionInParent = Point(taskX, taskY) + bounds.set(Rect(taskX, taskY, taskX + taskWidth, taskY + taskHeight)) + } + } fun createChange(mode: Int, taskInfo: ActivityManager.RunningTaskInfo): Change { val change = - Change( - WindowContainerToken(mock(IWindowContainerToken::class.java)), - mock(SurfaceControl::class.java)) + Change(WindowContainerToken(mock<IWindowContainerToken>()), mock<SurfaceControl>()) change.mode = mode change.taskInfo = taskInfo return change diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt index 310ccc252469..bc40d89009bc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -17,56 +17,111 @@ package com.android.wm.shell.desktopmode import android.graphics.Rect +import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner +import android.util.ArraySet import android.view.Display.DEFAULT_DISPLAY import android.view.Display.INVALID_DISPLAY import androidx.test.filters.SmallTest +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.desktopmode.persistence.Desktop +import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository +import com.android.wm.shell.sysui.ShellInit import com.google.common.truth.Truth.assertThat import junit.framework.Assert.fail +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.inOrder +import org.mockito.Mockito.spy +import org.mockito.kotlin.any +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidTestingRunner::class) +@ExperimentalCoroutinesApi class DesktopModeTaskRepositoryTest : ShellTestCase() { private lateinit var repo: DesktopModeTaskRepository + private lateinit var shellInit: ShellInit + private lateinit var datastoreScope: CoroutineScope + + @Mock private lateinit var testExecutor: ShellExecutor + @Mock private lateinit var persistentRepository: DesktopPersistentRepository @Before fun setUp() { - repo = DesktopModeTaskRepository() + Dispatchers.setMain(StandardTestDispatcher()) + datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + shellInit = spy(ShellInit(testExecutor)) + + repo = DesktopModeTaskRepository(context, shellInit, persistentRepository, datastoreScope) + whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn( + Desktop.getDefaultInstance() + ) + shellInit.init() + } + + @After + fun tearDown() { + datastoreScope.cancel() } @Test - fun addActiveTask_listenerNotifiedAndTaskIsActive() { + fun addActiveTask_notifiesListener() { val listener = TestListener() repo.addActiveTaskListener(listener) repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1) + assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(1) + } + + @Test + fun addActiveTask_taskIsActive() { + val listener = TestListener() + repo.addActiveTaskListener(listener) + + repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1) + assertThat(repo.isActiveTask(1)).isTrue() } @Test - fun addActiveTask_sameTaskDoesNotNotify() { + fun addSameActiveTaskTwice_notifiesOnce() { val listener = TestListener() repo.addActiveTaskListener(listener) repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1) repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1) + assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(1) } @Test - fun addActiveTask_multipleTasksAddedNotifiesForEach() { + fun addActiveTask_multipleTasksAdded_notifiesForAllTasks() { val listener = TestListener() repo.addActiveTaskListener(listener) repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1) repo.addActiveTask(DEFAULT_DISPLAY, taskId = 2) + assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(2) } @@ -84,22 +139,35 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test - fun removeActiveTask_listenerNotifiedAndTaskNotActive() { + fun removeActiveTask_notifiesListener() { val listener = TestListener() repo.addActiveTaskListener(listener) - repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1) + repo.removeActiveTask(1) + // Notify once for add and once for remove assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(2) + } + + @Test + fun removeActiveTask_taskNotActive() { + val listener = TestListener() + repo.addActiveTaskListener(listener) + repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1) + + repo.removeActiveTask(1) + assertThat(repo.isActiveTask(1)).isFalse() } @Test - fun removeActiveTask_removeNotExistingTaskDoesNotNotify() { + fun removeActiveTask_nonExistingTask_doesNotNotify() { val listener = TestListener() repo.addActiveTaskListener(listener) + repo.removeActiveTask(99) + assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(0) } @@ -108,72 +176,116 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { val listener = TestListener() repo.addActiveTaskListener(listener) repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1) + repo.removeActiveTask(1) + assertThat(listener.activeChangesOnSecondaryDisplay).isEqualTo(0) assertThat(repo.isActiveTask(1)).isFalse() } @Test - fun isActiveTask_notExistingTaskReturnsFalse() { + fun isActiveTask_nonExistingTask_returnsFalse() { assertThat(repo.isActiveTask(99)).isFalse() } @Test - fun isOnlyActiveTask_noActiveTasks() { - // Not an active task - assertThat(repo.isOnlyActiveTask(1)).isFalse() + fun isOnlyVisibleNonClosingTask_noTasks_returnsFalse() { + // No visible tasks + assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse() } @Test - fun isOnlyActiveTask_singleActiveTask() { - repo.addActiveTask(DEFAULT_DISPLAY, 1) - // The only active task - assertThat(repo.isActiveTask(1)).isTrue() - assertThat(repo.isOnlyActiveTask(1)).isTrue() - // Not an active task - assertThat(repo.isActiveTask(99)).isFalse() - assertThat(repo.isOnlyActiveTask(99)).isFalse() + fun isClosingTask_noTasks_returnsFalse() { + // No visible tasks + assertThat(repo.isClosingTask(1)).isFalse() + } + + @Test + fun updateTaskVisibility_singleVisibleNonClosingTask_updatesTasksCorrectly() { + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + + assertThat(repo.isVisibleTask(1)).isTrue() + assertThat(repo.isClosingTask(1)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(1)).isTrue() + + assertThat(repo.isVisibleTask(99)).isFalse() + assertThat(repo.isClosingTask(99)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse() + } + + @Test + fun isOnlyVisibleNonClosingTask_singleVisibleClosingTask() { + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.addClosingTask(DEFAULT_DISPLAY, 1) + + // A visible task that's closing + assertThat(repo.isVisibleTask(1)).isTrue() + assertThat(repo.isClosingTask(1)).isTrue() + assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse() + // Not a visible task + assertThat(repo.isVisibleTask(99)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse() + } + + @Test + fun isOnlyVisibleNonClosingTask_singleVisibleMinimizedTask() { + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.minimizeTask(DEFAULT_DISPLAY, 1) + + // The visible task that's closing + assertThat(repo.isVisibleTask(1)).isTrue() + assertThat(repo.isMinimizedTask(1)).isTrue() + assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse() + // Not a visible task + assertThat(repo.isVisibleTask(99)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse() } @Test - fun isOnlyActiveTask_multipleActiveTasks() { - repo.addActiveTask(DEFAULT_DISPLAY, 1) - repo.addActiveTask(DEFAULT_DISPLAY, 2) + fun isOnlyVisibleNonClosingTask_multipleVisibleNonClosingTasks() { + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = true) + // Not the only task - assertThat(repo.isActiveTask(1)).isTrue() - assertThat(repo.isOnlyActiveTask(1)).isFalse() + assertThat(repo.isVisibleTask(1)).isTrue() + assertThat(repo.isClosingTask(1)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse() // Not the only task - assertThat(repo.isActiveTask(2)).isTrue() - assertThat(repo.isOnlyActiveTask(2)).isFalse() - // Not an active task - assertThat(repo.isActiveTask(99)).isFalse() - assertThat(repo.isOnlyActiveTask(99)).isFalse() + assertThat(repo.isVisibleTask(2)).isTrue() + assertThat(repo.isClosingTask(2)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(2)).isFalse() + // Not a visible task + assertThat(repo.isVisibleTask(99)).isFalse() + assertThat(repo.isClosingTask(99)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse() } @Test - fun isOnlyActiveTask_multipleDisplays() { - repo.addActiveTask(DEFAULT_DISPLAY, 1) - repo.addActiveTask(DEFAULT_DISPLAY, 2) - repo.addActiveTask(SECOND_DISPLAY, 3) + fun isOnlyVisibleNonClosingTask_multipleDisplays() { + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = true) + repo.updateTaskVisibility(SECOND_DISPLAY, taskId = 3, visible = true) + // Not the only task on DEFAULT_DISPLAY - assertThat(repo.isActiveTask(1)).isTrue() - assertThat(repo.isOnlyActiveTask(1)).isFalse() + assertThat(repo.isVisibleTask(1)).isTrue() + assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse() // Not the only task on DEFAULT_DISPLAY - assertThat(repo.isActiveTask(2)).isTrue() - assertThat(repo.isOnlyActiveTask(2)).isFalse() - // The only active task on SECOND_DISPLAY - assertThat(repo.isActiveTask(3)).isTrue() - assertThat(repo.isOnlyActiveTask(3)).isTrue() - // Not an active task - assertThat(repo.isActiveTask(99)).isFalse() - assertThat(repo.isOnlyActiveTask(99)).isFalse() + assertThat(repo.isVisibleTask(2)).isTrue() + assertThat(repo.isOnlyVisibleNonClosingTask(2)).isFalse() + // The only visible task on SECOND_DISPLAY + assertThat(repo.isVisibleTask(3)).isTrue() + assertThat(repo.isOnlyVisibleNonClosingTask(3)).isTrue() + // Not a visible task + assertThat(repo.isVisibleTask(99)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse() } @Test - fun addListener_notifiesVisibleFreeformTask() { - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) + fun addVisibleTasksListener_notifiesVisibleFreeformTask() { + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) val listener = TestVisibilityListener() val executor = TestShellExecutor() + repo.addVisibleTasksListener(listener, executor) executor.flushAll() @@ -183,7 +295,7 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { @Test fun addListener_tasksOnDifferentDisplay_doesNotNotify() { - repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 1, visible = true) + repo.updateTaskVisibility(SECOND_DISPLAY, taskId = 1, visible = true) val listener = TestVisibilityListener() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) @@ -195,12 +307,13 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test - fun updateVisibleFreeformTasks_addVisibleTasksNotifiesListener() { + fun updateTaskVisibility_addVisibleTasksNotifiesListener() { val listener = TestVisibilityListener() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true) + + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = true) executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2) @@ -208,12 +321,12 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test - fun updateVisibleFreeformTasks_addVisibleTaskNotifiesListenerForThatDisplay() { + fun updateTaskVisibility_addVisibleTaskNotifiesListenerForThatDisplay() { val listener = TestVisibilityListener() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1) @@ -221,7 +334,7 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(0) assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(0) - repo.updateVisibleFreeformTasks(displayId = 1, taskId = 2, visible = true) + repo.updateTaskVisibility(displayId = 1, taskId = 2, visible = true) executor.flushAll() // Listener for secondary display is notified @@ -232,17 +345,17 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test - fun updateVisibleFreeformTasks_taskOnDefaultBecomesVisibleOnSecondDisplay_listenersNotified() { + fun updateTaskVisibility_taskOnDefaultBecomesVisibleOnSecondDisplay_listenersNotified() { val listener = TestVisibilityListener() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1) // Mark task 1 visible on secondary display - repo.updateVisibleFreeformTasks(displayId = 1, taskId = 1, visible = true) + repo.updateTaskVisibility(displayId = 1, taskId = 1, visible = true) executor.flushAll() // Default display should have 2 calls @@ -257,21 +370,22 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test - fun updateVisibleFreeformTasks_removeVisibleTasksNotifiesListener() { + fun updateTaskVisibility_removeVisibleTasksNotifiesListener() { val listener = TestVisibilityListener() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true) + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = true) executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2) - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = false) + + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = false) executor.flushAll() assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3) - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = false) + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = false) executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0) @@ -283,16 +397,17 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { * This tests that task is removed from the last parent display when it vanishes. */ @Test - fun updateVisibleFreeformTasks_removeVisibleTasksRemovesTaskWithInvalidDisplay() { + fun updateTaskVisibility_removeVisibleTasksRemovesTaskWithInvalidDisplay() { val listener = TestVisibilityListener() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true) + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = true) executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2) - repo.updateVisibleFreeformTasks(INVALID_DISPLAY, taskId = 1, visible = false) + + repo.updateTaskVisibility(INVALID_DISPLAY, taskId = 1, visible = false) executor.flushAll() assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3) @@ -300,63 +415,71 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test - fun getVisibleTaskCount() { + fun getVisibleTaskCount_defaultDisplay_returnsCorrectCount() { // No tasks, count is 0 assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) // New task increments count to 1 - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) // Visibility update to same task does not increase count - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) // Second task visible increments count - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true) + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = true) + assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2) // Hiding a task decrements count - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = false) + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = false) assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) // Hiding all tasks leaves count at 0 - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = false) + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = false) assertThat(repo.getVisibleTaskCount(displayId = 9)).isEqualTo(0) // Hiding a not existing task, count remains at 0 - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 999, visible = false) + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 999, visible = false) assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) } @Test - fun getVisibleTaskCount_multipleDisplays() { + fun getVisibleTaskCount_multipleDisplays_returnsCorrectCount() { assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(0) // New task on default display increments count for that display only - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(0) // New task on secondary display, increments count for that display only - repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 2, visible = true) + repo.updateTaskVisibility(SECOND_DISPLAY, taskId = 2, visible = true) + assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1) // Marking task visible on another display, updates counts for both displays - repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 1, visible = true) + repo.updateTaskVisibility(SECOND_DISPLAY, taskId = 1, visible = true) + assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(2) // Marking task that is on secondary display, hidden on default display, does not affect // secondary display - repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = false) + repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = false) + assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(2) // Hiding a task on that display, decrements count - repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 1, visible = false) + repo.updateTaskVisibility(SECOND_DISPLAY, taskId = 1, visible = false) + assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1) } @@ -375,6 +498,44 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun addOrMoveFreeformTaskToTop_noTaskExists_persistenceEnabled_addsToTop() = + runTest(StandardTestDispatcher()) { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7) + + val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) + assertThat(tasks).containsExactly(7, 6, 5).inOrder() + inOrder(persistentRepository).run { + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf(5) + ) + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf(6, 5) + ) + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf(7, 6, 5) + ) + } + } + + @Test fun addOrMoveFreeformTaskToTop_alreadyExists_movesToTop() { repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) @@ -388,18 +549,246 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test + fun addOrMoveFreeformTaskToTop_taskIsMinimized_unminimizesTask() { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7) + repo.minimizeTask(displayId = 0, taskId = 6) + + val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) + assertThat(tasks).containsExactly(7, 6, 5).inOrder() + assertThat(repo.isMinimizedTask(taskId = 6)).isTrue() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun minimizeTask_persistenceEnabled_taskIsPersistedAsMinimized() = + runTest(StandardTestDispatcher()) { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7) + + repo.minimizeTask(displayId = 0, taskId = 6) + + val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) + assertThat(tasks).containsExactly(7, 6, 5).inOrder() + assertThat(repo.isMinimizedTask(taskId = 6)).isTrue() + inOrder(persistentRepository).run { + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf(5) + ) + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf(6, 5) + ) + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf(7, 6, 5) + ) + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(arrayOf(6)), + freeformTasksInZOrder = arrayListOf(7, 6, 5) + ) + } + } + + @Test + fun addOrMoveFreeformTaskToTop_taskIsUnminimized_noop() { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7) + + val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) + assertThat(tasks).containsExactly(7, 6, 5).inOrder() + assertThat(repo.isMinimizedTask(taskId = 6)).isFalse() + } + + @Test + fun removeFreeformTask_invalidDisplay_removesTaskFromFreeformTasks() { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) + + repo.removeFreeformTask(INVALID_DISPLAY, taskId = 1) + + val invalidDisplayTasks = repo.getFreeformTasksInZOrder(INVALID_DISPLAY) + assertThat(invalidDisplayTasks).isEmpty() + val validDisplayTasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) + assertThat(validDisplayTasks).isEmpty() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun removeFreeformTask_invalidDisplay_persistenceEnabled_removesTaskFromFreeformTasks() { + runTest(StandardTestDispatcher()) { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) + + repo.removeFreeformTask(INVALID_DISPLAY, taskId = 1) + + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf(1) + ) + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = ArrayList() + ) + } + } + + @Test + fun removeFreeformTask_validDisplay_removesTaskFromFreeformTasks() { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) + + repo.removeFreeformTask(DEFAULT_DISPLAY, taskId = 1) + + val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) + assertThat(tasks).isEmpty() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun removeFreeformTask_validDisplay_persistenceEnabled_removesTaskFromFreeformTasks() { + runTest(StandardTestDispatcher()) { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) + + repo.removeFreeformTask(DEFAULT_DISPLAY, taskId = 1) + + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf(1) + ) + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = ArrayList() + ) + } + } + + @Test + fun removeFreeformTask_validDisplay_differentDisplay_doesNotRemovesTask() { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) + + repo.removeFreeformTask(SECOND_DISPLAY, taskId = 1) + + val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) + assertThat(tasks).containsExactly(1) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun removeFreeformTask_validDisplayButDifferentDisplay_persistenceEnabled_doesNotRemoveTask() { + runTest(StandardTestDispatcher()) { + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) + + repo.removeFreeformTask(SECOND_DISPLAY, taskId = 1) + + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf(1) + ) + verify(persistentRepository, never()) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = ArrayList() + ) + } + } + + @Test fun removeFreeformTask_removesTaskBoundsBeforeMaximize() { val taskId = 1 + repo.addActiveTask(THIRD_DISPLAY, taskId) + repo.addOrMoveFreeformTaskToTop(THIRD_DISPLAY, taskId) repo.saveBoundsBeforeMaximize(taskId, Rect(0, 0, 200, 200)) + repo.removeFreeformTask(THIRD_DISPLAY, taskId) + assertThat(repo.removeBoundsBeforeMaximize(taskId)).isNull() } @Test + fun removeFreeformTask_removesActiveTask() { + val taskId = 1 + val listener = TestListener() + repo.addActiveTaskListener(listener) + repo.addActiveTask(DEFAULT_DISPLAY, taskId) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId) + + repo.removeFreeformTask(THIRD_DISPLAY, taskId) + + assertThat(repo.isActiveTask(taskId)).isFalse() + assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(2) + } + + @Test + fun removeFreeformTask_unminimizesTask() { + val taskId = 1 + repo.addActiveTask(DEFAULT_DISPLAY, taskId) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId) + repo.minimizeTask(DEFAULT_DISPLAY, taskId) + + repo.removeFreeformTask(DEFAULT_DISPLAY, taskId) + + assertThat(repo.isMinimizedTask(taskId)).isFalse() + } + + @Test + fun removeFreeformTask_updatesTaskVisibility() { + val taskId = 1 + repo.addActiveTask(DEFAULT_DISPLAY, taskId) + repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId) + + repo.removeFreeformTask(THIRD_DISPLAY, taskId) + + assertThat(repo.isVisibleTask(taskId)).isFalse() + } + + @Test fun saveBoundsBeforeMaximize_boundsSavedByTaskId() { val taskId = 1 val bounds = Rect(0, 0, 200, 200) + repo.saveBoundsBeforeMaximize(taskId, bounds) + assertThat(repo.removeBoundsBeforeMaximize(taskId)).isEqualTo(bounds) } @@ -409,17 +798,20 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { val bounds = Rect(0, 0, 200, 200) repo.saveBoundsBeforeMaximize(taskId, bounds) repo.removeBoundsBeforeMaximize(taskId) - assertThat(repo.removeBoundsBeforeMaximize(taskId)).isNull() + + val boundsBeforeMaximize = repo.removeBoundsBeforeMaximize(taskId) + + assertThat(boundsBeforeMaximize).isNull() } @Test - fun minimizeTaskNotCalled_noTasksMinimized() { + fun isMinimizedTask_minimizeTaskNotCalled_noTasksMinimized() { assertThat(repo.isMinimizedTask(taskId = 0)).isFalse() assertThat(repo.isMinimizedTask(taskId = 1)).isFalse() } @Test - fun minimizeTask_onlyThatTaskIsMinimized() { + fun minimizeTask_minimizesCorrectTask() { repo.minimizeTask(displayId = 0, taskId = 0) assertThat(repo.isMinimizedTask(taskId = 0)).isTrue() @@ -428,8 +820,9 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test - fun unminimizeTask_taskNoLongerMinimized() { + fun unminimizeTask_unminimizesTask() { repo.minimizeTask(displayId = 0, taskId = 0) + repo.unminimizeTask(displayId = 0, taskId = 0) assertThat(repo.isMinimizedTask(taskId = 0)).isFalse() @@ -441,6 +834,7 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { fun unminimizeTask_nonExistentTask_doesntCrash() { repo.unminimizeTask(displayId = 0, taskId = 0) + // No change assertThat(repo.isMinimizedTask(taskId = 0)).isFalse() assertThat(repo.isMinimizedTask(taskId = 1)).isFalse() assertThat(repo.isMinimizedTask(taskId = 2)).isFalse() @@ -448,63 +842,44 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { @Test - fun updateVisibleFreeformTasks_toVisible_taskIsUnminimized() { + fun updateTaskVisibility_minimizedTaskBecomesVisible_unminimizesTask() { repo.minimizeTask(displayId = 10, taskId = 2) + repo.updateTaskVisibility(displayId = 10, taskId = 2, visible = true) - repo.updateVisibleFreeformTasks(displayId = 10, taskId = 2, visible = true) + val isMinimizedTask = repo.isMinimizedTask(taskId = 2) - assertThat(repo.isMinimizedTask(taskId = 2)).isFalse() + assertThat(isMinimizedTask).isFalse() } @Test - fun isDesktopModeShowing_noActiveTasks_returnsFalse() { - assertThat(repo.isDesktopModeShowing(displayId = 0)).isFalse() - } - - @Test - fun isDesktopModeShowing_noTasksVisible_returnsFalse() { - repo.addActiveTask(displayId = 0, taskId = 1) - repo.addActiveTask(displayId = 0, taskId = 2) - - assertThat(repo.isDesktopModeShowing(displayId = 0)).isFalse() - } - - @Test - fun isDesktopModeShowing_tasksActiveAndVisible_returnsTrue() { - repo.addActiveTask(displayId = 0, taskId = 1) - repo.addActiveTask(displayId = 0, taskId = 2) - repo.updateVisibleFreeformTasks(displayId = 0, taskId = 1, visible = true) - - assertThat(repo.isDesktopModeShowing(displayId = 0)).isTrue() - } - - @Test - fun getActiveNonMinimizedTasksOrderedFrontToBack_returnsFreeformTasksInCorrectOrder() { + fun getActiveNonMinimizedOrderedTasks_returnsFreeformTasksInCorrectOrder() { repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 1) repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 2) repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 3) - // The front-most task will be the one added last through addOrMoveFreeformTaskToTop + // The front-most task will be the one added last through `addOrMoveFreeformTaskToTop` repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 3) repo.addOrMoveFreeformTaskToTop(displayId = 0, taskId = 2) repo.addOrMoveFreeformTaskToTop(displayId = 0, taskId = 1) - assertThat(repo.getActiveNonMinimizedTasksOrderedFrontToBack(displayId = 0)) - .containsExactly(1, 2, 3).inOrder() + val tasks = repo.getActiveNonMinimizedOrderedTasks(displayId = 0) + + assertThat(tasks).containsExactly(1, 2, 3).inOrder() } @Test - fun getActiveNonMinimizedTasksOrderedFrontToBack_minimizedTaskNotIncluded() { + fun getActiveNonMinimizedOrderedTasks_excludesMinimizedTasks() { repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 1) repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 2) repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 3) - // The front-most task will be the one added last through addOrMoveFreeformTaskToTop + // The front-most task will be the one added last through `addOrMoveFreeformTaskToTop` repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 3) repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 2) repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 1) repo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = 2) - assertThat(repo.getActiveNonMinimizedTasksOrderedFrontToBack( - displayId = DEFAULT_DISPLAY)).containsExactly(1, 3).inOrder() + val tasks = repo.getActiveNonMinimizedOrderedTasks(displayId = DEFAULT_DISPLAY) + + assertThat(tasks).containsExactly(1, 3).inOrder() } @@ -545,5 +920,7 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { companion object { const val SECOND_DISPLAY = 1 const val THIRD_DISPLAY = 345 + private const val DEFAULT_USER_ID = 1000 + private const val DEFAULT_DESKTOP_ID = 0 } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt index 518c00d377ad..db4e93de9541 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt @@ -18,11 +18,6 @@ package com.android.wm.shell.desktopmode import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.APP_FROM_OVERVIEW -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.KEYBOARD_SHORTCUT -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.TASK_DRAG -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.UNKNOWN import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG @@ -33,6 +28,11 @@ import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getEnterTransitionType import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getExitTransitionType +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_FROM_OVERVIEW +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.KEYBOARD_SHORTCUT +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.TASK_DRAG +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUtilsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUtilsTest.kt new file mode 100644 index 000000000000..8fab410adc30 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUtilsTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 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.desktopmode + +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopModeUtilsTest { + @Test + fun isTaskBoundsEqual_stableBoundsAreEqual_returnTrue() { + assertThat(isTaskBoundsEqual(task2Bounds, stableBounds)).isTrue() + } + + @Test + fun isTaskBoundsEqual_stableBoundsAreNotEqual_returnFalse() { + assertThat(isTaskBoundsEqual(task4Bounds, stableBounds)).isFalse() + } + + @Test + fun isTaskWidthOrHeightEqual_stableBoundsAreEqual_returnTrue() { + assertThat(isTaskWidthOrHeightEqual(task2Bounds, stableBounds)).isTrue() + } + + @Test + fun isTaskWidthOrHeightEqual_stableBoundWidthIsEquals_returnTrue() { + assertThat(isTaskWidthOrHeightEqual(task3Bounds, stableBounds)).isTrue() + } + + @Test + fun isTaskWidthOrHeightEqual_stableBoundHeightIsEquals_returnTrue() { + assertThat(isTaskWidthOrHeightEqual(task3Bounds, stableBounds)).isTrue() + } + + @Test + fun isTaskWidthOrHeightEqual_stableBoundsWidthOrHeightAreNotEquals_returnFalse() { + assertThat(isTaskWidthOrHeightEqual(task1Bounds, stableBounds)).isTrue() + } + + private companion object { + val task1Bounds = Rect(0, 0, 0, 0) + val task2Bounds = Rect(1, 1, 1, 1) + val task3Bounds = Rect(0, 1, 0, 1) + val task4Bounds = Rect(1, 2, 2, 1) + val stableBounds = Rect(1, 1, 1, 1) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt index bd39aa6ace42..2b7f86f36477 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt @@ -17,14 +17,12 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RunningTaskInfo -import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM -import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN -import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.graphics.Rect import android.graphics.Region import android.testing.AndroidTestingRunner import android.view.SurfaceControl import androidx.test.filters.SmallTest +import com.android.internal.policy.SystemBarUtils import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase @@ -38,6 +36,11 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.kotlin.whenever +/** + * Test class for [DesktopModeVisualIndicator] + * + * Usage: atest WMShellUnitTests:DesktopModeVisualIndicatorTest + */ @SmallTest @RunWith(AndroidTestingRunner::class) class DesktopModeVisualIndicatorTest : ShellTestCase() { @@ -52,8 +55,6 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { @Before fun setUp() { - visualIndicator = DesktopModeVisualIndicator(syncQueue, taskInfo, displayController, - context, taskSurface, taskDisplayAreaOrganizer) whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width()) whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height()) whenever(displayLayout.stableInsets()).thenReturn(STABLE_INSETS) @@ -61,38 +62,51 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { @Test fun testFullscreenRegionCalculation() { - val transitionHeight = context.resources.getDimensionPixelSize( - R.dimen.desktop_mode_fullscreen_from_desktop_height) - val fromFreeformWidth = mContext.resources.getDimensionPixelSize( - R.dimen.desktop_mode_fullscreen_from_desktop_width - ) - var testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, - WINDOWING_MODE_FULLSCREEN, CAPTION_HEIGHT) + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) + var testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, CAPTION_HEIGHT) assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, 2 * STABLE_INSETS.top)) - testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, - WINDOWING_MODE_FREEFORM, CAPTION_HEIGHT) + + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FREEFORM) + testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, CAPTION_HEIGHT) + val transitionHeight = SystemBarUtils.getStatusBarHeight(context) + val toFullscreenScale = mContext.resources.getFloat( + R.dimen.desktop_mode_fullscreen_region_scale + ) + val toFullscreenWidth = displayLayout.width() * toFullscreenScale assertThat(testRegion.bounds).isEqualTo(Rect( - DISPLAY_BOUNDS.width() / 2 - fromFreeformWidth / 2, + (DISPLAY_BOUNDS.width() / 2f - toFullscreenWidth / 2f).toInt(), -50, - DISPLAY_BOUNDS.width() / 2 + fromFreeformWidth / 2, + (DISPLAY_BOUNDS.width() / 2f + toFullscreenWidth / 2f).toInt(), transitionHeight)) - testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, - WINDOWING_MODE_MULTI_WINDOW, CAPTION_HEIGHT) + + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT) + testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, CAPTION_HEIGHT) assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, 2 * STABLE_INSETS.top)) + + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT) + testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, CAPTION_HEIGHT) + assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, transitionHeight)) } @Test fun testSplitLeftRegionCalculation() { val transitionHeight = context.resources.getDimensionPixelSize( R.dimen.desktop_mode_split_from_desktop_height) + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) var testRegion = visualIndicator.calculateSplitLeftRegion(displayLayout, - WINDOWING_MODE_FULLSCREEN, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 32, 1600)) + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FREEFORM) testRegion = visualIndicator.calculateSplitLeftRegion(displayLayout, - WINDOWING_MODE_FREEFORM, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) assertThat(testRegion.bounds).isEqualTo(Rect(0, transitionHeight, 32, 1600)) + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT) + testRegion = visualIndicator.calculateSplitLeftRegion(displayLayout, + TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 32, 1600)) + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT) testRegion = visualIndicator.calculateSplitLeftRegion(displayLayout, - WINDOWING_MODE_MULTI_WINDOW, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 32, 1600)) } @@ -100,27 +114,35 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { fun testSplitRightRegionCalculation() { val transitionHeight = context.resources.getDimensionPixelSize( R.dimen.desktop_mode_split_from_desktop_height) + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) var testRegion = visualIndicator.calculateSplitRightRegion(displayLayout, - WINDOWING_MODE_FULLSCREEN, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) assertThat(testRegion.bounds).isEqualTo(Rect(2368, -50, 2400, 1600)) + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FREEFORM) testRegion = visualIndicator.calculateSplitRightRegion(displayLayout, - WINDOWING_MODE_FREEFORM, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) assertThat(testRegion.bounds).isEqualTo(Rect(2368, transitionHeight, 2400, 1600)) + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT) + testRegion = visualIndicator.calculateSplitRightRegion(displayLayout, + TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + assertThat(testRegion.bounds).isEqualTo(Rect(2368, -50, 2400, 1600)) + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT) testRegion = visualIndicator.calculateSplitRightRegion(displayLayout, - WINDOWING_MODE_MULTI_WINDOW, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) assertThat(testRegion.bounds).isEqualTo(Rect(2368, -50, 2400, 1600)) } @Test fun testToDesktopRegionCalculation() { + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) val fullscreenRegion = visualIndicator.calculateFullscreenRegion(displayLayout, - WINDOWING_MODE_FULLSCREEN, CAPTION_HEIGHT) + CAPTION_HEIGHT) val splitLeftRegion = visualIndicator.calculateSplitLeftRegion(displayLayout, - WINDOWING_MODE_FULLSCREEN, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) val splitRightRegion = visualIndicator.calculateSplitRightRegion(displayLayout, - WINDOWING_MODE_FULLSCREEN, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) val desktopRegion = visualIndicator.calculateToDesktopRegion(displayLayout, - WINDOWING_MODE_FULLSCREEN, splitLeftRegion, splitRightRegion, fullscreenRegion) + splitLeftRegion, splitRightRegion, fullscreenRegion) var testRegion = Region() testRegion.union(DISPLAY_BOUNDS) testRegion.op(splitLeftRegion, Region.Op.DIFFERENCE) @@ -129,6 +151,11 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { assertThat(desktopRegion).isEqualTo(testRegion) } + private fun createVisualIndicator(dragStartState: DesktopModeVisualIndicator.DragStartState) { + visualIndicator = DesktopModeVisualIndicator(syncQueue, taskInfo, displayController, + context, taskSurface, taskDisplayAreaOrganizer, dragStartState) + } + companion object { private const val TRANSITION_AREA_WIDTH = 32 private const val CAPTION_HEIGHT = 50 diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index e17f7f2f7b12..ee545209904f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -18,6 +18,9 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RecentTaskInfo import android.app.ActivityManager.RunningTaskInfo +import android.app.ActivityOptions +import android.app.KeyguardManager +import android.app.PendingIntent import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM @@ -37,14 +40,18 @@ import android.graphics.Point import android.graphics.PointF import android.graphics.Rect import android.os.Binder +import android.os.Handler import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY +import android.view.DragEvent +import android.view.Gravity import android.view.SurfaceControl import android.view.WindowManager import android.view.WindowManager.TRANSIT_CHANGE +import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT @@ -64,8 +71,11 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.ExtendedMockito.never import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.internal.jank.InteractionJankMonitor import com.android.window.flags.Flags +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE import com.android.wm.shell.MockToken +import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase @@ -77,17 +87,21 @@ import com.android.wm.shell.common.LaunchAdjacentController import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.UNKNOWN -import com.android.wm.shell.common.split.SplitScreenConstants +import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition +import com.android.wm.shell.desktopmode.DesktopTasksController.TaskbarDesktopTaskListener import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createHomeTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createSplitScreenTask +import com.android.wm.shell.desktopmode.persistence.Desktop +import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.recents.RecentTasksController import com.android.wm.shell.recents.RecentsTransitionHandler import com.android.wm.shell.recents.RecentsTransitionStateListener -import com.android.wm.shell.shared.DesktopModeStatus +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN +import com.android.wm.shell.shared.split.SplitScreenConstants import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController @@ -99,9 +113,20 @@ import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS import com.android.wm.shell.transition.Transitions.TransitionHandler import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage +import java.util.function.Consumer import java.util.Optional import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Assume.assumeTrue import org.junit.Before @@ -109,21 +134,21 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers.eq import org.mockito.ArgumentMatchers.isA import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock import org.mockito.Mockito -import org.mockito.Mockito.any import org.mockito.Mockito.anyInt import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.mock import org.mockito.Mockito.spy import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever +import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.capture +import org.mockito.kotlin.eq +import org.mockito.kotlin.whenever import org.mockito.quality.Strictness /** @@ -133,6 +158,8 @@ import org.mockito.quality.Strictness */ @SmallTest @RunWith(AndroidTestingRunner::class) +@ExperimentalCoroutinesApi +@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) class DesktopTasksControllerTest : ShellTestCase() { @JvmField @Rule val setFlagsRule = SetFlagsRule() @@ -146,8 +173,11 @@ class DesktopTasksControllerTest : ShellTestCase() { @Mock lateinit var syncQueue: SyncTransactionQueue @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock lateinit var transitions: Transitions + @Mock lateinit var keyguardManager: KeyguardManager + @Mock lateinit var mReturnToDragStartAnimator: ReturnToDragStartAnimator @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler + @Mock lateinit var dragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler @Mock lateinit var toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler @Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler @@ -159,13 +189,20 @@ class DesktopTasksControllerTest : ShellTestCase() { @Mock lateinit var desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator @Mock lateinit var recentTasksController: RecentTasksController + @Mock + private lateinit var mockInteractionJankMonitor: InteractionJankMonitor + @Mock private lateinit var mockSurface: SurfaceControl + @Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener + @Mock private lateinit var mockHandler: Handler + @Mock lateinit var persistentRepository: DesktopPersistentRepository private lateinit var mockitoSession: StaticMockitoSession private lateinit var controller: DesktopTasksController private lateinit var shellInit: ShellInit - private lateinit var desktopModeTaskRepository: DesktopModeTaskRepository + private lateinit var taskRepository: DesktopModeTaskRepository private lateinit var desktopTasksLimiter: DesktopTasksLimiter private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener + private lateinit var testScope: CoroutineScope private val shellExecutor = TestShellExecutor() @@ -174,27 +211,35 @@ class DesktopTasksControllerTest : ShellTestCase() { private val DISPLAY_DIMENSION_SHORT = 1600 private val DISPLAY_DIMENSION_LONG = 2560 - private val DEFAULT_LANDSCAPE_BOUNDS = Rect(320, 200, 2240, 1400) - private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 320, 1400, 2240) - private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 680, 1575, 1880) - private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 200, 1880, 1400) - private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 699, 1575, 1861) - private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 200, 1730, 1400) + private val DEFAULT_LANDSCAPE_BOUNDS = Rect(320, 75, 2240, 1275) + private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 165, 1400, 2085) + private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 435, 1575, 1635) + private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 75, 1880, 1275) + private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 449, 1575, 1611) + private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 75, 1730, 1275) @Before fun setUp() { + Dispatchers.setMain(StandardTestDispatcher()) mockitoSession = mockitoSession() .strictness(Strictness.LENIENT) .spyStatic(DesktopModeStatus::class.java) .startMocking() - whenever(DesktopModeStatus.isEnabled()).thenReturn(true) doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) - desktopModeTaskRepository = DesktopModeTaskRepository() + taskRepository = DesktopModeTaskRepository(context, shellInit, persistentRepository, testScope) desktopTasksLimiter = - DesktopTasksLimiter(transitions, desktopModeTaskRepository, shellTaskOrganizer) + DesktopTasksLimiter( + transitions, + taskRepository, + shellTaskOrganizer, + MAX_TASK_LIMIT, + mockInteractionJankMonitor, + mContext, + mockHandler) whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } @@ -203,6 +248,9 @@ class DesktopTasksControllerTest : ShellTestCase() { whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> (i.arguments.first() as Rect).set(STABLE_BOUNDS) } + whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn( + Desktop.getDefaultInstance() + ) val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN @@ -216,6 +264,8 @@ class DesktopTasksControllerTest : ShellTestCase() { val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java) verify(recentsTransitionHandler).addTransitionStateListener(captor.capture()) recentsTransitionStateListener = captor.value + + controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener } private fun createController(): DesktopTasksController { @@ -230,18 +280,24 @@ class DesktopTasksControllerTest : ShellTestCase() { rootTaskDisplayAreaOrganizer, dragAndDropController, transitions, + keyguardManager, + mReturnToDragStartAnimator, enterDesktopTransitionHandler, exitDesktopTransitionHandler, + dragAndDropTransitionHandler, toggleResizeDesktopTaskTransitionHandler, dragToDesktopTransitionHandler, - desktopModeTaskRepository, + taskRepository, desktopModeLoggerTransitionObserver, launchAdjacentController, recentsTransitionHandler, multiInstanceHelper, shellExecutor, Optional.of(desktopTasksLimiter), - recentTasksController) + recentTasksController, + mockInteractionJankMonitor, + mockHandler, + ) } @After @@ -249,6 +305,7 @@ class DesktopTasksControllerTest : ShellTestCase() { mockitoSession.finishMocking() runningTasks.clear() + testScope.cancel() } @Test @@ -257,8 +314,54 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun instantiate_flagOff_doNotAddInitCallback() { - whenever(DesktopModeStatus.isEnabled()).thenReturn(false) + fun doesAnyTaskRequireTaskbarRounding_onlyFreeFormTaskIsRunning_returnFalse() { + setUpFreeformTask() + + assertThat(controller.doesAnyTaskRequireTaskbarRounding(DEFAULT_DISPLAY)).isFalse() + } + + @Test + fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfFreeFormTask_returnTrue() { + val task1 = setUpFreeformTask() + + val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) + controller.toggleDesktopTaskSize(task1) + verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture()) + + assertThat(argumentCaptor.value).isTrue() + } + + @Test + fun doesAnyTaskRequireTaskbarRounding_fullScreenTaskIsRunning_returnTrue() { + val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } + setUpFreeformTask(bounds = stableBounds, active = true) + assertThat(controller.doesAnyTaskRequireTaskbarRounding(DEFAULT_DISPLAY)).isTrue() + } + + @Test + fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfFullScreenTask_returnFalse() { + val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } + val task1 = setUpFreeformTask(bounds = stableBounds, active = true) + + val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) + controller.toggleDesktopTaskSize(task1) + verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture()) + + assertThat(argumentCaptor.value).isFalse() + } + + @Test + fun doesAnyTaskRequireTaskbarRounding_splitScreenTaskIsRunning_returnTrue() { + val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } + setUpFreeformTask(bounds = Rect(stableBounds.left, stableBounds.top, 500, stableBounds.bottom)) + + assertThat(controller.doesAnyTaskRequireTaskbarRounding(DEFAULT_DISPLAY)).isTrue() + } + + + @Test + fun instantiate_cannotEnterDesktopMode_doNotAddInitCallback() { + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false) clearInvocations(shellInit) createController() @@ -304,6 +407,50 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun isDesktopModeShowing_noTasks_returnsFalse() { + assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse() + } + + @Test + fun isDesktopModeShowing_noTasksVisible_returnsFalse() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskHidden(task2) + + assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse() + } + + @Test + fun isDesktopModeShowing_tasksActiveAndVisible_returnsTrue() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskVisible(task1) + markTaskHidden(task2) + + assertThat(controller.isDesktopModeShowing(displayId = 0)).isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() { + val homeTask = setUpHomeTask(SECOND_DISPLAY) + val task1 = setUpFreeformTask(SECOND_DISPLAY) + val task2 = setUpFreeformTask(SECOND_DISPLAY) + markTaskHidden(task1) + markTaskHidden(task2) + + controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: home, task1, task2 (no wallpaper intent) + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() @@ -323,6 +470,25 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_onSecondaryDisplay_desktopWallpaperDisabled_shouldNotMoveLauncher() { + val homeTask = setUpHomeTask(SECOND_DISPLAY) + val task1 = setUpFreeformTask(SECOND_DISPLAY) + val task2 = setUpFreeformTask(SECOND_DISPLAY) + markTaskHidden(task1) + markTaskHidden(task2) + + controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: home, task1, task2 + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() { val task1 = setUpFreeformTask() @@ -420,6 +586,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() { + val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) setUpHomeTask(SECOND_DISPLAY) val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY) @@ -429,21 +596,25 @@ class DesktopTasksControllerTest : ShellTestCase() { controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(2) - // Expect order to be from bottom: wallpaper intent, task - wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) - wct.assertReorderAt(index = 1, taskDefaultDisplay) + assertThat(wct.hierarchyOps).hasSize(3) + // Move home to front + wct.assertReorderAt(index = 0, homeTaskDefaultDisplay) + // Add desktop wallpaper activity + wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent) + // Move freeform task to front + wct.assertReorderAt(index = 2, taskDefaultDisplay) } @Test - fun showDesktopApps_dontReorderMinimizedTask() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_desktopWallpaperDisabled_dontReorderMinimizedTask() { val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() val minimizedTask = setUpFreeformTask() + markTaskHidden(freeformTask) markTaskHidden(minimizedTask) - desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId) - + taskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId) controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) @@ -454,163 +625,370 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun getVisibleTaskCount_noTasks_returnsZero() { - assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() { + val homeTask = setUpHomeTask() + val freeformTask = setUpFreeformTask() + val minimizedTask = setUpFreeformTask() + + markTaskHidden(freeformTask) + markTaskHidden(minimizedTask) + taskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId) + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Move home to front + wct.assertReorderAt(index = 0, homeTask, toTop = true) + // Add desktop wallpaper activity + wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent) + // Reorder freeform task to top, don't reorder the minimized task + wct.assertReorderAt(index = 2, freeformTask, toTop = true) + } + + @Test + fun visibleTaskCount_noTasks_returnsZero() { + assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) } @Test - fun getVisibleTaskCount_twoTasks_bothVisible_returnsTwo() { + fun visibleTaskCount_twoTasks_bothVisible_returnsTwo() { setUpHomeTask() setUpFreeformTask().also(::markTaskVisible) setUpFreeformTask().also(::markTaskVisible) - assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2) + assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2) } @Test - fun getVisibleTaskCount_twoTasks_oneVisible_returnsOne() { + fun visibleTaskCount_twoTasks_oneVisible_returnsOne() { setUpHomeTask() setUpFreeformTask().also(::markTaskVisible) setUpFreeformTask().also(::markTaskHidden) - assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) + assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) } @Test - fun getVisibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() { + fun visibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() { setUpHomeTask() setUpFreeformTask(DEFAULT_DISPLAY).also(::markTaskVisible) setUpFreeformTask(SECOND_DISPLAY).also(::markTaskVisible) - assertThat(controller.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1) + assertThat(controller.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(1) } @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { + fun addMoveToDesktopChanges_gravityLeft_noBoundsApplied() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask(gravity = Gravity.LEFT) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(finalBounds).isEqualTo(Rect()) + } + + @Test + fun addMoveToDesktopChanges_gravityRight_noBoundsApplied() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask(gravity = Gravity.RIGHT) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(finalBounds).isEqualTo(Rect()) + } + + @Test + fun addMoveToDesktopChanges_gravityTop_noBoundsApplied() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask(gravity = Gravity.TOP) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(finalBounds).isEqualTo(Rect()) + } + + @Test + fun addMoveToDesktopChanges_gravityBottom_noBoundsApplied() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask(gravity = Gravity.BOTTOM) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(finalBounds).isEqualTo(Rect()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun handleRequest_newFreeformTaskLaunch_cascadeApplied() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS) + val freeformTask = setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS, active = false) + + val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) + + assertNotNull(wct, "should handle request") + val finalBounds = findBoundsChange(wct, freeformTask) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.BottomRight) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun handleRequest_freeformTaskAlreadyExistsInDesktopMode_cascadeNotApplied() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS) + val freeformTask = setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS) + + val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) + + assertNull(wct, "should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_positionBottomRight() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS) + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.BottomRight) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_positionTopLeft() { setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + addFreeformTaskAtPosition(DesktopTaskPosition.BottomRight, stableBounds) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.TopLeft) } @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() { - val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_positionBottomLeft() { setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + addFreeformTaskAtPosition(DesktopTaskPosition.TopLeft, stableBounds) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.BottomLeft) } @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() { - val task = - setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true) + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_positionTopRight() { setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS) + addFreeformTaskAtPosition(DesktopTaskPosition.BottomLeft, stableBounds) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.TopRight) } @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() { - val task = - setUpFullscreenTask(isResizable = false, screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_positionResetsToCenter() { setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + addFreeformTaskAtPosition(DesktopTaskPosition.TopRight, stableBounds) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.Center) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_lastWindowSnapLeft_positionResetsToCenter() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + // Add freeform task with half display size snap bounds at left side. + setUpFreeformTask(bounds = Rect(stableBounds.left, stableBounds.top, 500, stableBounds.bottom)) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.Center) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_lastWindowSnapRight_positionResetsToCenter() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + // Add freeform task with half display size snap bounds at right side. + setUpFreeformTask(bounds = Rect( + stableBounds.right - 500, stableBounds.top, stableBounds.right, stableBounds.bottom)) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.Center) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_lastWindowMaximised_positionResetsToCenter() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + // Add maximised freeform task. + setUpFreeformTask(bounds = Rect(stableBounds)) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.Center) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_defaultToCenterIfFree() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + val minTouchTarget = context.resources.getDimensionPixelSize( + R.dimen.freeform_required_visible_empty_space_in_header) + addFreeformTaskAtPosition(DesktopTaskPosition.Center, stableBounds, + Rect(0, 0, 1600, 1200), Point(0, minTouchTarget + 1)) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.Center) } @Test @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() { - val task = - setUpFullscreenTask( - isResizable = false, - screenOrientation = SCREEN_ORIENTATION_PORTRAIT, - shouldLetterbox = true) + fun addMoveToDesktopChanges_landscapeDevice_userFullscreenOverride_defaultPortraitBounds() { setUpLandscapeDisplay() + val task = setUpFullscreenTask(enableUserFullscreenOverride = true) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS) + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) } @Test @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() { - val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT) - setUpPortraitDisplay() + fun addMoveToDesktopChanges_landscapeDevice_systemFullscreenOverride_defaultPortraitBounds() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask(enableSystemFullscreenOverride = true) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) } @Test @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() { - val task = - setUpFullscreenTask( - deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_PORTRAIT) - setUpPortraitDisplay() + fun addMoveToDesktopChanges_landscapeDevice_portraitResizableApp_aspectRatioOverridden() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, + shouldLetterbox = true, aspectRatioOverrideApplied = true) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS) } @Test @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() { - val task = - setUpFullscreenTask( - deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, - shouldLetterbox = true) + fun addMoveToDesktopChanges_portraitDevice_userFullscreenOverride_defaultPortraitBounds() { setUpPortraitDisplay() + val task = setUpFullscreenTask(enableUserFullscreenOverride = true) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS) + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) } @Test @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() { - val task = - setUpFullscreenTask( - isResizable = false, - deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_PORTRAIT) + fun addMoveToDesktopChanges_portraitDevice_systemFullscreenOverride_defaultPortraitBounds() { setUpPortraitDisplay() + val task = setUpFullscreenTask(enableSystemFullscreenOverride = true) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) } @Test @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() { - val task = - setUpFullscreenTask( - isResizable = false, - deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, - shouldLetterbox = true) + fun addMoveToDesktopChanges_portraitDevice_landscapeResizableApp_aspectRatioOverridden() { setUpPortraitDisplay() + val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, + deviceOrientation = ORIENTATION_PORTRAIT, + shouldLetterbox = true, aspectRatioOverrideApplied = true) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS) } @@ -619,57 +997,92 @@ class DesktopTasksControllerTest : ShellTestCase() { val task = setUpFullscreenTask() val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - controller.moveToDesktop(task, transitionSource = UNKNOWN) + controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) } @Test - fun moveToDesktop_tdaFreeform_windowingModeSetToUndefined() { + fun moveRunningTaskToDesktop_tdaFreeform_windowingModeSetToUndefined() { val task = setUpFullscreenTask() val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM - controller.moveToDesktop(task, transitionSource = UNKNOWN) + controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_UNDEFINED) } @Test - fun moveToDesktop_nonExistentTask_doesNothing() { - controller.moveToDesktop(999, transitionSource = UNKNOWN) + fun moveTaskToDesktop_nonExistentTask_doesNothing() { + controller.moveTaskToDesktop(999, transitionSource = UNKNOWN) verifyEnterDesktopWCTNotExecuted() } @Test - fun moveToDesktop_nonRunningTask_launchesInFreeform() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveTaskToDesktop_desktopWallpaperDisabled_nonRunningTask_launchesInFreeform() { + val task = createTaskInfo(1) whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) + whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) - val task = createTaskInfo(1) + controller.moveTaskToDesktop(task.taskId, transitionSource = UNKNOWN) + + with(getLatestEnterDesktopWct()) { + assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) + } + } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveTaskToDesktop_desktopWallpaperEnabled_nonRunningTask_launchesInFreeform() { + val task = createTaskInfo(1) + whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) - controller.moveToDesktop(task.taskId, transitionSource = UNKNOWN) + controller.moveTaskToDesktop(task.taskId, transitionSource = UNKNOWN) + with(getLatestEnterDesktopWct()) { - assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) + // Add desktop wallpaper activity + assertPendingIntentAt(index = 0, desktopWallpaperIntent) + // Launch task + assertLaunchTaskAt(index = 1, task.taskId, WINDOWING_MODE_FREEFORM) } } @Test - fun moveToDesktop_topActivityTranslucent_doesNothing() { - setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun moveRunningTaskToDesktop_topActivityTranslucentWithStyleFloating_taskIsMovedToDesktop() { val task = - setUpFullscreenTask().apply { - isTopActivityTransparent = true - numActivities = 1 - } + setUpFullscreenTask().apply { + isTopActivityTransparent = true + isTopActivityStyleFloating = true + numActivities = 1 + } + + controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun moveRunningTaskToDesktop_topActivityTranslucentWithoutStyleFloating_doesNothing() { + val task = + setUpFullscreenTask().apply { + isTopActivityTransparent = true + isTopActivityStyleFloating = false + numActivities = 1 + } - controller.moveToDesktop(task, transitionSource = UNKNOWN) + controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) verifyEnterDesktopWCTNotExecuted() } @Test - fun moveToDesktop_systemUIActivity_doesNothing() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun moveRunningTaskToDesktop_systemUIActivity_doesNothing() { val task = setUpFullscreenTask() // Set task as systemUI package @@ -678,15 +1091,15 @@ class DesktopTasksControllerTest : ShellTestCase() { val baseComponent = ComponentName(systemUIPackageName, /* class */ "") task.baseActivity = baseComponent - controller.moveToDesktop(task, transitionSource = UNKNOWN) + controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) verifyEnterDesktopWCTNotExecuted() } @Test - fun moveToDesktop_deviceSupported_taskIsMovedToDesktop() { + fun moveRunningTaskToDesktop_deviceSupported_taskIsMovedToDesktop() { val task = setUpFullscreenTask() - controller.moveToDesktop(task, transitionSource = UNKNOWN) + controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) @@ -694,13 +1107,13 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun moveToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperDisabled() { + fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() val fullscreenTask = setUpFullscreenTask() markTaskHidden(freeformTask) - controller.moveToDesktop(fullscreenTask, transitionSource = UNKNOWN) + controller.moveRunningTaskToDesktop(fullscreenTask, transitionSource = UNKNOWN) with(getLatestEnterDesktopWct()) { // Operations should include home task, freeform task @@ -713,12 +1126,12 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun moveToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() { + fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() { val freeformTask = setUpFreeformTask() val fullscreenTask = setUpFullscreenTask() markTaskHidden(freeformTask) - controller.moveToDesktop(fullscreenTask, transitionSource = UNKNOWN) + controller.moveRunningTaskToDesktop(fullscreenTask, transitionSource = UNKNOWN) with(getLatestEnterDesktopWct()) { // Operations should include wallpaper intent, freeform task, fullscreen task @@ -732,7 +1145,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun moveToDesktop_onlyFreeformTasksFromCurrentDisplayBroughtToFront() { + fun moveRunningTaskToDesktop_onlyFreeformTasksFromCurrentDisplayBroughtToFront() { setUpHomeTask(displayId = DEFAULT_DISPLAY) val freeformTaskDefault = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val fullscreenTaskDefault = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) @@ -742,7 +1155,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY) markTaskHidden(freeformTaskSecond) - controller.moveToDesktop(fullscreenTaskDefault, transitionSource = UNKNOWN) + controller.moveRunningTaskToDesktop(fullscreenTaskDefault, transitionSource = UNKNOWN) with(getLatestEnterDesktopWct()) { // Check that hierarchy operations do not include tasks from second display @@ -753,9 +1166,9 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun moveToDesktop_splitTaskExitsSplit() { + fun moveRunningTaskToDesktop_splitTaskExitsSplit() { val task = setUpSplitScreenTask() - controller.moveToDesktop(task, transitionSource = UNKNOWN) + controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) verify(splitScreenController) @@ -763,9 +1176,9 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun moveToDesktop_fullscreenTaskDoesNotExitSplit() { + fun moveRunningTaskToDesktop_fullscreenTaskDoesNotExitSplit() { val task = setUpFullscreenTask() - controller.moveToDesktop(task, transitionSource = UNKNOWN) + controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) verify(splitScreenController, never()) @@ -773,21 +1186,43 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun moveToDesktop_bringsTasksOverLimit_dontShowBackTask() { - val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveRunningTaskToDesktop_desktopWallpaperDisabled_bringsTasksOver_dontShowBackTask() { + val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } + val newTask = setUpFullscreenTask() val homeTask = setUpHomeTask() - val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + + controller.moveRunningTaskToDesktop(newTask, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + assertThat(wct.hierarchyOps.size).isEqualTo(MAX_TASK_LIMIT + 1) // visible tasks + home + wct.assertReorderAt(0, homeTask) + wct.assertReorderSequenceInRange( + range = 1..<(MAX_TASK_LIMIT + 1), + *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0] + newTask) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveRunningTaskToDesktop_desktopWallpaperEnabled_bringsTasksOverLimit_dontShowBackTask() { + val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } val newTask = setUpFullscreenTask() + val homeTask = setUpHomeTask() - controller.moveToDesktop(newTask, transitionSource = UNKNOWN) + controller.moveRunningTaskToDesktop(newTask, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() - assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 1) // visible tasks + home + assertThat(wct.hierarchyOps.size).isEqualTo(MAX_TASK_LIMIT + 2) // tasks + home + wallpaper + // Move home to front wct.assertReorderAt(0, homeTask) - for (i in 1..<taskLimit) { // Skipping freeformTasks[0] - wct.assertReorderAt(index = i, task = freeformTasks[i]) - } - wct.assertReorderAt(taskLimit, newTask) + // Add desktop wallpaper activity + wct.assertPendingIntentAt(1, desktopWallpaperIntent) + // Bring freeform tasks to front + wct.assertReorderSequenceInRange( + range = 2..<(MAX_TASK_LIMIT + 2), + *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0] + newTask) } @Test @@ -802,6 +1237,24 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task.token.asBinder()]) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) + // Removes wallpaper activity when leaving desktop + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test fun moveToFullscreen_tdaFreeform_windowingModeSetToFullscreen() { val task = setUpFreeformTask() val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! @@ -813,6 +1266,44 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task.token.asBinder()]) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN) + // Removes wallpaper activity when leaving desktop + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() { + val task1 = setUpFreeformTask() + // Setup task2 + setUpFreeformTask() + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + + controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val task1Change = assertNotNull(wct.changes[task1.token.asBinder()]) + assertThat(task1Change.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) + // Does not remove wallpaper activity, as desktop still has a visible desktop task + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test fun moveToFullscreen_nonExistentTask_doesNothing() { controller.moveToFullscreen(999, transitionSource = UNKNOWN) verifyExitDesktopWCTNotExecuted() @@ -845,9 +1336,8 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun moveTaskToFront_bringsTasksOverLimit_minimizesBackTask() { - val taskLimit = desktopTasksLimiter.getMaxTaskLimit() setUpHomeTask() - val freeformTasks = (1..taskLimit + 1).map { _ -> setUpFreeformTask() } + val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() } controller.moveTaskToFront(freeformTasks[0]) @@ -923,7 +1413,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun onDesktopWindowClose_noActiveTasks() { val wct = WindowContainerTransaction() - controller.onDesktopWindowClose(wct, 1 /* taskId */) + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = 1) // Doesn't modify transaction assertThat(wct.hierarchyOps).isEmpty() } @@ -932,7 +1422,7 @@ class DesktopTasksControllerTest : ShellTestCase() { fun onDesktopWindowClose_singleActiveTask_noWallpaperActivityToken() { val task = setUpFreeformTask() val wct = WindowContainerTransaction() - controller.onDesktopWindowClose(wct, task.taskId) + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task.taskId) // Doesn't modify transaction assertThat(wct.hierarchyOps).isEmpty() } @@ -941,38 +1431,196 @@ class DesktopTasksControllerTest : ShellTestCase() { fun onDesktopWindowClose_singleActiveTask_hasWallpaperActivityToken() { val task = setUpFreeformTask() val wallpaperToken = MockToken().token() - desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.wallpaperActivityToken = wallpaperToken val wct = WindowContainerTransaction() - controller.onDesktopWindowClose(wct, task.taskId) + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task.taskId) // Adds remove wallpaper operation wct.assertRemoveAt(index = 0, wallpaperToken) } @Test + fun onDesktopWindowClose_singleActiveTask_isClosing() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.addClosingTask(DEFAULT_DISPLAY, task.taskId) + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowClose_singleActiveTask_isMinimized() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId) + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test fun onDesktopWindowClose_multipleActiveTasks() { val task1 = setUpFreeformTask() setUpFreeformTask() val wallpaperToken = MockToken().token() - desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.wallpaperActivityToken = wallpaperToken val wct = WindowContainerTransaction() - controller.onDesktopWindowClose(wct, task1.taskId) + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task1.taskId) // Doesn't modify transaction assertThat(wct.hierarchyOps).isEmpty() } @Test + fun onDesktopWindowClose_multipleActiveTasks_isOnlyNonClosingTask() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.addClosingTask(DEFAULT_DISPLAY, task2.taskId) + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task1.taskId) + // Adds remove wallpaper operation + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun onDesktopWindowClose_multipleActiveTasks_hasMinimized() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId) + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task1.taskId) + // Adds remove wallpaper operation + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun onDesktopWindowMinimize_noActiveTask_doesntUpdateTransaction() { + val wct = WindowContainerTransaction() + controller.onDesktopWindowMinimize(wct, taskId = 1) + // Nothing happens. + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntUpdateTransaction() { + val task = setUpFreeformTask() + val wct = WindowContainerTransaction() + controller.onDesktopWindowMinimize(wct, taskId = task.taskId) + // Nothing happens. + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowMinimize_singleActiveTask_hasWallpaperActivityToken_removesWallpaper() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + + val wct = WindowContainerTransaction() + // The only active task is being minimized. + controller.onDesktopWindowMinimize(wct, taskId = task.taskId) + // Adds remove wallpaper operation + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun onDesktopWindowMinimize_singleActiveTask_alreadyMinimized_doesntUpdateTransaction() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId) + + val wct = WindowContainerTransaction() + // The only active task is already minimized. + controller.onDesktopWindowMinimize(wct, taskId = task.taskId) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowMinimize_multipleActiveTasks_doesntUpdateTransaction() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + + val wct = WindowContainerTransaction() + controller.onDesktopWindowMinimize(wct, taskId = task1.taskId) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowMinimize_multipleActiveTasks_minimizesTheOnlyVisibleTask_removesWallpaper() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId) + + val wct = WindowContainerTransaction() + // task1 is the only visible task as task2 is minimized. + controller.onDesktopWindowMinimize(wct, taskId = task1.taskId) + // Adds remove wallpaper operation + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() { assumeTrue(ENABLE_SHELL_TRANSITIONS) + val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() markTaskVisible(freeformTask) val fullscreenTask = createFullscreenTask() - val result = controller.handleRequest(Binder(), createTransition(fullscreenTask)) - assertThat(result?.changes?.get(fullscreenTask.token.asBinder())?.windowingMode) + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct, "should handle request") + assertThat(wct.changes[fullscreenTask.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FREEFORM) + + assertThat(wct.hierarchyOps).hasSize(1) + } + + @Test + fun handleRequest_fullscreenTaskWithTaskOnHome_freeformVisible_returnSwitchToFreeformWCT() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val homeTask = setUpHomeTask() + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + val fullscreenTask = createFullscreenTask() + fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct, "should handle request") + assertThat(wct.changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + + // There are 5 hops that are happening in this case: + // 1. Moving the fullscreen task to top as we add moveToDesktop() changes + // 2. Bringing home task to front + // 3. Pending intent for the wallpaper + // 4. Bringing the existing freeform task to top + // 5. Bringing the fullscreen task back at the top + assertThat(wct.hierarchyOps).hasSize(5) + wct.assertReorderAt(1, homeTask, toTop = true) + wct.assertReorderAt(4, fullscreenTask, toTop = true) } @Test @@ -994,8 +1642,7 @@ class DesktopTasksControllerTest : ShellTestCase() { fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() { assumeTrue(ENABLE_SHELL_TRANSITIONS) - val taskLimit = desktopTasksLimiter.getMaxTaskLimit() - val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } freeformTasks.forEach { markTaskVisible(it) } val fullscreenTask = createFullscreenTask() @@ -1008,6 +1655,75 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun handleRequest_fullscreenTaskWithTaskOnHome_bringsTasksOverLimit_otherTaskIsMinimized() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } + freeformTasks.forEach { markTaskVisible(it) } + val fullscreenTask = createFullscreenTask() + fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + // Make sure we reorder the new task to top, and the back task to the bottom + assertThat(wct!!.hierarchyOps.size).isEqualTo(9) + wct.assertReorderAt(0, fullscreenTask, toTop = true) + wct.assertReorderAt(8, freeformTasks[0], toTop = false) + } + + @Test + fun handleRequest_fullscreenTaskWithTaskOnHome_beyondLimit_existingAndNewTasksAreMinimized() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val minimizedTask = setUpFreeformTask() + taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = minimizedTask.taskId) + val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } + freeformTasks.forEach { markTaskVisible(it) } + val homeTask = setUpHomeTask() + val fullscreenTask = createFullscreenTask() + fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertThat(wct!!.hierarchyOps.size).isEqualTo(10) + wct.assertReorderAt(0, fullscreenTask, toTop = true) + // Make sure we reorder the home task to the top, desktop tasks to top of them and minimized + // task is under the home task. + wct.assertReorderAt(1, homeTask, toTop = true) + wct.assertReorderAt(9, freeformTasks[0], toTop = false) + } + + @Test + fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + + val fullscreenTask = createFullscreenTask() + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct, "should handle request") + assertThat(wct.changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + assertThat(wct.hierarchyOps).hasSize(1) + wct.assertReorderAt(0, fullscreenTask, toTop = true) + } + + @Test + fun handleRequest_fullscreenTask_noTasks_enforceDesktop_fullscreenDisplay_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + + val fullscreenTask = createFullscreenTask() + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertThat(wct).isNull() + } + + @Test fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() { assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -1040,8 +1756,7 @@ class DesktopTasksControllerTest : ShellTestCase() { fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() { assumeTrue(ENABLE_SHELL_TRANSITIONS) - val taskLimit = desktopTasksLimiter.getMaxTaskLimit() - val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } freeformTasks.forEach { markTaskVisible(it) } val newFreeformTask = createFreeformTask() @@ -1052,41 +1767,153 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun handleRequest_freeformTask_freeformNotVisible_reorderedToTop() { + fun handleRequest_freeformTask_relaunchActiveTask_taskBecomesUndefined() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTask = setUpFreeformTask() + markTaskHidden(freeformTask) + + val wct = + controller.handleRequest(Binder(), createTransition(freeformTask)) + + // Should become undefined as the TDA is set to fullscreen. It will inherit from the TDA. + assertNotNull(wct, "should handle request") + assertThat(wct.changes[freeformTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test + fun handleRequest_freeformTask_relaunchTask_enforceDesktop_freeformDisplay_noWinModeChange() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + + val freeformTask = setUpFreeformTask() + markTaskHidden(freeformTask) + val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) + + assertNotNull(wct, "should handle request") + assertFalse(wct.anyWindowingModeChange(freeformTask.token)) + } + + @Test + fun handleRequest_freeformTask_relaunchTask_enforceDesktop_fullscreenDisplay_becomesUndefined() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + + val freeformTask = setUpFreeformTask() + markTaskHidden(freeformTask) + val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) + + assertNotNull(wct, "should handle request") + assertThat(wct.changes[freeformTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() { assumeTrue(ENABLE_SHELL_TRANSITIONS) val freeformTask1 = setUpFreeformTask() + val freeformTask2 = createFreeformTask() + markTaskHidden(freeformTask1) + val result = + controller.handleRequest(Binder(), createTransition(freeformTask2, type = TRANSIT_TO_FRONT)) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(2) + result.assertReorderAt(1, freeformTask2, toTop = true) + } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTask1 = setUpFreeformTask() val freeformTask2 = createFreeformTask() + + markTaskHidden(freeformTask1) val result = - controller.handleRequest(Binder(), createTransition(freeformTask2, type = TRANSIT_TO_FRONT)) + controller.handleRequest(Binder(), createTransition(freeformTask2, type = TRANSIT_TO_FRONT)) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(3) + // Add desktop wallpaper activity + result.assertPendingIntentAt(0, desktopWallpaperIntent) + // Bring active desktop tasks to front + result.assertReorderAt(1, freeformTask1, toTop = true) + // Bring new task to front + result.assertReorderAt(2, freeformTask2, toTop = true) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val task = createFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task)) - assertThat(result?.hierarchyOps?.size).isEqualTo(2) - result!!.assertReorderAt(1, freeformTask2, toTop = true) + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(1) + result.assertReorderAt(0, task, toTop = true) } @Test - fun handleRequest_freeformTask_noOtherTasks_reorderedToTop() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() { assumeTrue(ENABLE_SHELL_TRANSITIONS) val task = createFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task)) - assertThat(result?.hierarchyOps?.size).isEqualTo(1) - result!!.assertReorderAt(0, task, toTop = true) + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(2) + // Add desktop wallpaper activity + result.assertPendingIntentAt(0, desktopWallpaperIntent) + // Bring new task to front + result.assertReorderAt(1, task, toTop = true) } @Test - fun handleRequest_freeformTask_freeformOnOtherDisplayOnly_reorderedToTop() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_dskWallpaperDisabled_freeformOnOtherDisplayOnly_reorderedToTop() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) + // Second display task + createFreeformTask(displayId = SECOND_DISPLAY) + + val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay)) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(1) + result.assertReorderAt(0, taskDefaultDisplay, toTop = true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() { assumeTrue(ENABLE_SHELL_TRANSITIONS) val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) - val taskSecondDisplay = createFreeformTask(displayId = SECOND_DISPLAY) + // Second display task + createFreeformTask(displayId = SECOND_DISPLAY) val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay)) - assertThat(result?.hierarchyOps?.size).isEqualTo(1) - result!!.assertReorderAt(0, taskDefaultDisplay, toTop = true) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(2) + // Add desktop wallpaper activity + result.assertPendingIntentAt(0, desktopWallpaperIntent) + // Bring new task to front + result.assertReorderAt(1, taskDefaultDisplay, toTop = true) } @Test @@ -1118,6 +1945,17 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun handleRequest_freeformTask_keyguardLocked_returnNull() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + whenever(keyguardManager.isKeyguardLocked).thenReturn(true) + val freeformTask = createFreeformTask(displayId = DEFAULT_DISPLAY) + + val result = controller.handleRequest(Binder(), createTransition(freeformTask)) + + assertNull(result, "Should NOT handle request") + } + + @Test fun handleRequest_notOpenOrToFrontTransition_returnNull() { assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -1126,7 +1964,7 @@ class DesktopTasksControllerTest : ShellTestCase() { .setActivityType(ACTIVITY_TYPE_STANDARD) .setWindowingMode(WINDOWING_MODE_FULLSCREEN) .build() - val transition = createTransition(task = task, type = WindowManager.TRANSIT_CLOSE) + val transition = createTransition(task = task, type = TRANSIT_CLOSE) val result = controller.handleRequest(Binder(), transition) assertThat(result).isNull() } @@ -1171,20 +2009,55 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun handleRequest_shouldLaunchAsModal_returnSwitchToFullscreenWCT() { - setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun handleRequest_recentsAnimationRunning_relaunchActiveTask_taskBecomesUndefined() { + // Set up a visible freeform task + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + + // Mark recents animation running + recentsTransitionStateListener.onAnimationStateChanged(true) + + // Should become undefined as the TDA is set to fullscreen. It will inherit from the TDA. + val result = controller.handleRequest(Binder(), createTransition(freeformTask)) + assertThat(result?.changes?.get(freeformTask.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun handleRequest_topActivityTransparentWithStyleFloating_returnSwitchToFreeformWCT() { + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + val task = - setUpFreeformTask().apply { - isTopActivityTransparent = true - numActivities = 1 - } + setUpFullscreenTask().apply { + isTopActivityTransparent = true + isTopActivityStyleFloating = true + numActivities = 1 + } val result = controller.handleRequest(Binder(), createTransition(task)) assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun handleRequest_topActivityTransparentWithoutStyleFloating_returnSwitchToFullscreenWCT() { + val task = + setUpFreeformTask().apply { + isTopActivityTransparent = true + isTopActivityStyleFloating = false + numActivities = 1 + } + + val result = controller.handleRequest(Binder(), createTransition(task)) + assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) fun handleRequest_systemUIActivity_returnSwitchToFullscreenWCT() { val task = setUpFreeformTask() @@ -1200,69 +2073,447 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_singleActiveTask_noToken() { + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + ) + fun handleRequest_backTransition_singleTaskNoToken_noWallpaper_noBackNav_doesNotHandle() { val task = setUpFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - // Doesn't handle request - assertThat(result).isNull() + + assertNull(result, "Should not handle request") } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_singleActiveTask_hasToken_desktopWallpaperDisabled() { - desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_withBackNav_removesTask() { + val task = setUpFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + + assertNotNull(result, "Should handle request").assertRemoveAt(0, task.token) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + ) + fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_notInDesktop_doesNotHandle() { val task = setUpFreeformTask() + markTaskHidden(task) + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - // Doesn't handle request - assertThat(result).isNull() + + assertNull(result, "Should not handle request") } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_singleActiveTask_hasToken_desktopWallpaperEnabled() { - val wallpaperToken = MockToken().token() - desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_backTransition_singleTaskNoToken_noBackNav_doesNotHandle() { + val task = setUpFreeformTask() + + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } + + @Test + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_backTransition_singleTaskWithToken_noWallpaper_noBackNav_doesNotHandle() { + val task = setUpFreeformTask() + + taskRepository.wallpaperActivityToken = MockToken().token() + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_backTransition_singleTask_withWallpaper_withBackNav_removesWallpaperAndTask() { val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - assertThat(result).isNotNull() - // Creates remove wallpaper transaction - result!!.assertRemoveAt(index = 0, wallpaperToken) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + result.assertRemoveAt(index = 1, task.token) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_multipleActiveTasks() { - desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_backTransition_singleTaskWithToken_noBackNav_removesWallpaper() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_backTransition_multipleTasks_noWallpaper_noBackNav_doesNotHandle() { val task1 = setUpFreeformTask() setUpFreeformTask() + + taskRepository.wallpaperActivityToken = MockToken().token() val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) - // Doesn't handle request - assertThat(result).isNull() + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_backTransition_multipleTasks_withWallpaper_withBackNav_removesTask() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + + taskRepository.wallpaperActivityToken = MockToken().token() + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, task1.token) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_backTransition_multipleTasks_noBackNav_doesNotHandle() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + + taskRepository.wallpaperActivityToken = MockToken().token() + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_backTransition_multipleTasksSingleNonClosing_removesWallpaperAndTask() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + result.assertRemoveAt(index = 1, task1.token) } @Test - fun desktopTasksVisibilityChange_visible_setLaunchAdjacentDisabled() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_backTransition_multipleTasksSingleNonClosing_noBackNav_removesWallpaper() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_backTransition_multipleTasksSingleNonMinimized_removesWallpaperAndTask() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + result.assertRemoveAt(index = 1, task1.token) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_backTransition_multipleTasksSingleNonMinimized_noBackNav_removesWallpaper() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_backTransition_nonMinimizadTask_withWallpaper_withBackNav_removesWallpaper() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + // Task is being minimized so mark it as not visible. + taskRepository + .updateTaskVisibility(displayId = DEFAULT_DISPLAY, task2.taskId, false) + val result = controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } + + @Test + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_closeTransition_singleTaskNoToken_noWallpaper_noBackNav_doesNotHandle() { val task = setUpFreeformTask() - clearInvocations(launchAdjacentController) - markTaskVisible(task) - shellExecutor.flushAll() - verify(launchAdjacentController).launchAdjacentEnabled = false + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) + + assertNull(result, "Should not handle request") } @Test - fun desktopTasksVisibilityChange_invisible_setLaunchAdjacentEnabled() { + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_closeTransition_singleTaskNoToken_withWallpaper_withBackNav_removesTask() { val task = setUpFreeformTask() - markTaskVisible(task) - clearInvocations(launchAdjacentController) - markTaskHidden(task) - shellExecutor.flushAll() - verify(launchAdjacentController).launchAdjacentEnabled = true + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) + + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, task.token) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_closeTransition_singleTaskNoToken_noBackNav_doesNotHandle() { + val task = setUpFreeformTask() + + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) + + assertNull(result, "Should not handle request") + } + + @Test + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_closeTransition_singleTaskWithToken_noWallpaper_noBackNav_doesNotHandle() { + val task = setUpFreeformTask() + + taskRepository.wallpaperActivityToken = MockToken().token() + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_closeTransition_singleTaskWithToken_removesWallpaperAndTask() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + result.assertRemoveAt(index = 1, task.token) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_closeTransition_singleTaskWithToken_withWallpaper_noBackNav_removesWallpaper() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_closeTransition_multipleTasks_noWallpaper_noBackNav_doesNotHandle() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + + taskRepository.wallpaperActivityToken = MockToken().token() + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_closeTransition_multipleTasks_withWallpaper_withBackNav_removesTask() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + + taskRepository.wallpaperActivityToken = MockToken().token() + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) + + assertNotNull(result, "Should handle request") + result.assertRemoveAt(index = 0, task1.token) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_closeTransition_multipleTasksFlagEnabled_noBackNav_doesNotHandle() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + + taskRepository.wallpaperActivityToken = MockToken().token() + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_closeTransition_multipleTasksSingleNonClosing_removesWallpaperAndTask() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + result.assertRemoveAt(index = 1, task1.token) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_closeTransition_multipleTasksSingleNonClosing_noBackNav_removesWallpaper() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_closeTransition_multipleTasksOneNonMinimized_removesWallpaperAndTask() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + result.assertRemoveAt(index = 1, task1.token) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_noBackNav_removesWallpaper() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION + ) + fun handleRequest_closeTransition_minimizadTask_withWallpaper_withBackNav_removesWallpaper() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + // Task is being minimized so mark it as not visible. + taskRepository + .updateTaskVisibility(displayId = DEFAULT_DISPLAY, task2.taskId, false) + val result = controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") } @Test @@ -1323,17 +2574,60 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun moveFocusedTaskToFullscreen_onlyVisibleNonMinimizedTask_removesWallpaperActivity() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId) + taskRepository.updateTaskVisibility(DEFAULT_DISPLAY, task3.taskId, + visible = false) + + controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + taskRepository.wallpaperActivityToken = wallpaperToken + controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + // Does not remove wallpaper activity, as desktop still has visible desktop tasks + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { val spyController = spy(controller) whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) val task = setUpFullscreenTask() setUpLandscapeDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) } @@ -1343,13 +2637,13 @@ class DesktopTasksControllerTest : ShellTestCase() { fun dragToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() { val spyController = spy(controller) whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) setUpLandscapeDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) } @@ -1359,14 +2653,14 @@ class DesktopTasksControllerTest : ShellTestCase() { fun dragToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() { val spyController = spy(controller) whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true) setUpLandscapeDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS) } @@ -1376,14 +2670,14 @@ class DesktopTasksControllerTest : ShellTestCase() { fun dragToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() { val spyController = spy(controller) whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) val task = setUpFullscreenTask(isResizable = false, screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) setUpLandscapeDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) } @@ -1393,7 +2687,7 @@ class DesktopTasksControllerTest : ShellTestCase() { fun dragToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() { val spyController = spy(controller) whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) val task = @@ -1403,7 +2697,7 @@ class DesktopTasksControllerTest : ShellTestCase() { shouldLetterbox = true) setUpLandscapeDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS) } @@ -1413,13 +2707,13 @@ class DesktopTasksControllerTest : ShellTestCase() { fun dragToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() { val spyController = spy(controller) whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT) setUpPortraitDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) } @@ -1429,7 +2723,7 @@ class DesktopTasksControllerTest : ShellTestCase() { fun dragToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() { val spyController = spy(controller) whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) val task = @@ -1438,7 +2732,7 @@ class DesktopTasksControllerTest : ShellTestCase() { screenOrientation = SCREEN_ORIENTATION_PORTRAIT) setUpPortraitDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) } @@ -1448,7 +2742,7 @@ class DesktopTasksControllerTest : ShellTestCase() { fun dragToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() { val spyController = spy(controller) whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) val task = @@ -1458,7 +2752,7 @@ class DesktopTasksControllerTest : ShellTestCase() { shouldLetterbox = true) setUpPortraitDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS) } @@ -1468,7 +2762,7 @@ class DesktopTasksControllerTest : ShellTestCase() { fun dragToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() { val spyController = spy(controller) whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) val task = @@ -1478,7 +2772,7 @@ class DesktopTasksControllerTest : ShellTestCase() { screenOrientation = SCREEN_ORIENTATION_PORTRAIT) setUpPortraitDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) } @@ -1488,7 +2782,7 @@ class DesktopTasksControllerTest : ShellTestCase() { fun dragToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() { val spyController = spy(controller) whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull())) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) val task = @@ -1499,7 +2793,7 @@ class DesktopTasksControllerTest : ShellTestCase() { shouldLetterbox = true) setUpPortraitDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task) + spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS) } @@ -1515,10 +2809,12 @@ class DesktopTasksControllerTest : ShellTestCase() { controller.onDragPositioningEnd( task, + mockSurface, Point(100, -100), /* position */ PointF(200f, -200f), /* inputCoordinate */ - Rect(100, -100, 500, 1000), /* taskBounds */ - Rect(0, 50, 2000, 2000) /* validDragArea */) + Rect(100, -100, 500, 1000), /* currentDragBounds */ + Rect(0, 50, 2000, 2000), /* validDragArea */ + Rect() /* dragStartBounds */ ) val rectAfterEnd = Rect(100, 50, 500, 1150) verify(transitions) .startTransition( @@ -1531,6 +2827,43 @@ class DesktopTasksControllerTest : ShellTestCase() { eq(null)) } + @Test + fun onDesktopDragEnd_noIndicator_updatesTaskBounds() { + val task = setUpFreeformTask() + val spyController = spy(controller) + val mockSurface = mock(SurfaceControl::class.java) + val mockDisplayLayout = mock(DisplayLayout::class.java) + whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout) + whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000)) + spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000)) + + val currentDragBounds = Rect(100, 200, 500, 1000) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR) + + spyController.onDragPositioningEnd( + task, + mockSurface, + Point(100, 200), /* position */ + PointF(200f, 300f), /* inputCoordinate */ + currentDragBounds, /* currentDragBounds */ + Rect(0, 50, 2000, 2000) /* validDragArea */, + Rect() /* dragStartBounds */) + + + verify(transitions) + .startTransition( + eq(TRANSIT_CHANGE), + Mockito.argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + change.configuration.windowConfiguration.bounds == currentDragBounds + } + }, + eq(null)) + } + + @Test fun enterSplit_freeformTaskIsMovedToSplit() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -1540,14 +2873,67 @@ class DesktopTasksControllerTest : ShellTestCase() { task2.isFocused = true task3.isFocused = false - controller.enterSplit(DEFAULT_DISPLAY, false) + controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) verify(splitScreenController) .requestEnterSplitSelect( - task2, + eq(task2), any(), - SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT, - task2.configuration.windowConfiguration.bounds) + eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), + eq(task2.configuration.windowConfiguration.bounds)) + } + + @Test + fun enterSplit_onlyVisibleNonMinimizedTask_removesWallpaperActivity() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId) + taskRepository.updateTaskVisibility(DEFAULT_DISPLAY, task3.taskId, + visible = false) + + controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) + + val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(splitScreenController) + .requestEnterSplitSelect( + eq(task2), + wctArgument.capture(), + eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), + eq(task2.configuration.windowConfiguration.bounds)) + // Removes wallpaper activity when leaving desktop + wctArgument.value.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun enterSplit_multipleVisibleNonMinimizedTasks_removesWallpaperActivity() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + taskRepository.wallpaperActivityToken = wallpaperToken + + controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) + + val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(splitScreenController) + .requestEnterSplitSelect( + eq(task2), + wctArgument.capture(), + eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), + eq(task2.configuration.windowConfiguration.bounds)) + // Does not remove wallpaper activity, as desktop still has visible desktop tasks + assertThat(wctArgument.value.hierarchyOps).isEmpty() } @Test @@ -1562,12 +2948,124 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun snapToHalfScreen_getSnapBounds_calculatesBoundsForResizable() { + val bounds = Rect(100, 100, 300, 300) + val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply { + topActivityInfo = ActivityInfo().apply { + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE + configuration.windowConfiguration.appBounds = bounds + } + isResizeable = true + } + + val currentDragBounds = Rect(0, 100, 200, 300) + val expectedBounds = Rect( + STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom + ) + + controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT) + // Assert bounds set to stable bounds + val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds) + assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds) + } + + @Test + fun snapToHalfScreen_snapBoundsWhenAlreadySnapped_animatesSurfaceWithoutWCT() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + // Set up task to already be in snapped-left bounds + val bounds = Rect( + STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom + ) + val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply { + topActivityInfo = ActivityInfo().apply { + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE + configuration.windowConfiguration.appBounds = bounds + } + isResizeable = true + } + + // Attempt to snap left again + val currentDragBounds = Rect(bounds).apply { offset(-100, 0) } + controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT) + + // Assert that task is NOT updated via WCT + verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any()) + + // Assert that task leash is updated via Surface Animations + verify(mReturnToDragStartAnimator).start( + eq(task.taskId), + eq(mockSurface), + eq(currentDragBounds), + eq(bounds), + eq(true) + ) + } + + @Test + @DisableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING) + fun handleSnapResizingTask_nonResizable_snapsToHalfScreen() { + val task = setUpFreeformTask(DEFAULT_DISPLAY, Rect(0, 0, 200, 100)).apply { + isResizeable = false + } + val preDragBounds = Rect(100, 100, 400, 500) + val currentDragBounds = Rect(0, 100, 300, 500) + + controller.handleSnapResizingTask( + task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds) + val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds) + assertThat(findBoundsChange(wct, task)).isEqualTo( + Rect(STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom)) + } + + @Test + @EnableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING) + fun handleSnapResizingTask_nonResizable_startsRepositionAnimation() { + val task = setUpFreeformTask(DEFAULT_DISPLAY, Rect(0, 0, 200, 100)).apply { + isResizeable = false + } + val preDragBounds = Rect(100, 100, 400, 500) + val currentDragBounds = Rect(0, 100, 300, 500) + + controller.handleSnapResizingTask( + task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds) + verify(mReturnToDragStartAnimator).start( + eq(task.taskId), + eq(mockSurface), + eq(currentDragBounds), + eq(preDragBounds), + eq(false) + ) + } + + @Test + fun toggleBounds_togglesToCalculatedBoundsForNonResizable() { + val bounds = Rect(0, 0, 200, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply { + topActivityInfo = ActivityInfo().apply { + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE + configuration.windowConfiguration.appBounds = bounds + } + appCompatTaskInfo.topActivityLetterboxAppWidth = bounds.width() + appCompatTaskInfo.topActivityLetterboxAppHeight = bounds.height() + isResizeable = false + } + + // Bounds should be 1000 x 500, vertically centered in the 1000 x 1000 stable bounds + val expectedBounds = Rect(STABLE_BOUNDS.left, 250, STABLE_BOUNDS.right, 750) + + controller.toggleDesktopTaskSize(task) + // Assert bounds set to stable bounds + val wct = getLatestToggleResizeDesktopTaskWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds) + } + + @Test fun toggleBounds_lastBoundsBeforeMaximizeSaved() { val bounds = Rect(0, 0, 100, 100) val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds) controller.toggleDesktopTaskSize(task) - assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId)).isEqualTo(bounds) + assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isEqualTo(bounds) } @Test @@ -1588,6 +3086,46 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize_nonResizeableEqualWidth() { + val boundsBeforeMaximize = Rect(0, 0, 100, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize).apply { + isResizeable = false + } + + // Maximize + controller.toggleDesktopTaskSize(task) + task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS.left, + boundsBeforeMaximize.top, STABLE_BOUNDS.right, boundsBeforeMaximize.bottom) + + // Restore + controller.toggleDesktopTaskSize(task) + + // Assert bounds set to last bounds before maximize + val wct = getLatestToggleResizeDesktopTaskWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize) + } + + @Test + fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize_nonResizeableEqualHeight() { + val boundsBeforeMaximize = Rect(0, 0, 100, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize).apply { + isResizeable = false + } + + // Maximize + controller.toggleDesktopTaskSize(task) + task.configuration.windowConfiguration.bounds.set(boundsBeforeMaximize.left, + STABLE_BOUNDS.top, boundsBeforeMaximize.right, STABLE_BOUNDS.bottom) + + // Restore + controller.toggleDesktopTaskSize(task) + + // Assert bounds set to last bounds before maximize + val wct = getLatestToggleResizeDesktopTaskWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize) + } + + @Test fun toggleBounds_removesLastBoundsBeforeMaximizeAfterRestoringBounds() { val boundsBeforeMaximize = Rect(0, 0, 100, 100) val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize) @@ -1600,22 +3138,127 @@ class DesktopTasksControllerTest : ShellTestCase() { controller.toggleDesktopTaskSize(task) // Assert last bounds before maximize removed after use - assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull() + assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull() + } + + + @Test + fun onUnhandledDrag_newFreeformIntent() { + testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR, + PointF(1200f, 700f), + Rect(240, 700, 2160, 1900)) + } + + @Test + fun onUnhandledDrag_newFreeformIntentSplitLeft() { + testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR, + PointF(50f, 700f), + Rect(0, 0, 500, 1000)) + } + + @Test + fun onUnhandledDrag_newFreeformIntentSplitRight() { + testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR, + PointF(2500f, 700f), + Rect(500, 0, 1000, 1000)) + } + + @Test + fun onUnhandledDrag_newFullscreenIntent() { + testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + PointF(1200f, 50f), + Rect()) + } + + /** + * Assert that an unhandled drag event launches a PendingIntent with the + * windowing mode and bounds we are expecting. + */ + private fun testOnUnhandledDrag( + indicatorType: DesktopModeVisualIndicator.IndicatorType, + inputCoordinate: PointF, + expectedBounds: Rect + ) { + setUpLandscapeDisplay() + val task = setUpFreeformTask() + markTaskVisible(task) + task.isFocused = true + val runningTasks = ArrayList<RunningTaskInfo>() + runningTasks.add(task) + val spyController = spy(controller) + val mockPendingIntent = mock(PendingIntent::class.java) + val mockDragEvent = mock(DragEvent::class.java) + val mockCallback = mock(Consumer::class.java) + val b = SurfaceControl.Builder() + b.setName("test surface") + val dragSurface = b.build() + whenever(shellTaskOrganizer.runningTasks).thenReturn(runningTasks) + whenever(mockDragEvent.dragSurface).thenReturn(dragSurface) + whenever(mockDragEvent.x).thenReturn(inputCoordinate.x) + whenever(mockDragEvent.y).thenReturn(inputCoordinate.y) + whenever(multiInstanceHelper.supportsMultiInstanceSplit(anyOrNull())).thenReturn(true) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + doReturn(indicatorType) + .whenever(spyController).updateVisualIndicator( + eq(task), + anyOrNull(), + anyOrNull(), + anyOrNull(), + eq(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT) + ) + + spyController.onUnhandledDrag( + mockPendingIntent, + mockDragEvent, + mockCallback as Consumer<Boolean> + ) + val arg: ArgumentCaptor<WindowContainerTransaction> = + ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + var expectedWindowingMode: Int + if (indicatorType == DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) { + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN + // Fullscreen launches currently use default transitions + verify(transitions).startTransition(any(), capture(arg), anyOrNull()) + } else { + expectedWindowingMode = WINDOWING_MODE_FREEFORM + // All other launches use a special handler. + verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg)) + } + assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions) + .launchWindowingMode).isEqualTo(expectedWindowingMode) + assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions) + .launchBounds).isEqualTo(expectedBounds) } private val desktopWallpaperIntent: Intent get() = Intent(context, DesktopWallpaperActivity::class.java) + private fun addFreeformTaskAtPosition( + pos: DesktopTaskPosition, + stableBounds: Rect, + bounds: Rect = DEFAULT_LANDSCAPE_BOUNDS, + offsetPos: Point = Point(0, 0) + ): RunningTaskInfo { + val offset = pos.getTopLeftCoordinates(stableBounds, bounds) + val prevTaskBounds = Rect(bounds) + prevTaskBounds.offsetTo(offset.x + offsetPos.x, offset.y + offsetPos.y) + return setUpFreeformTask(bounds = prevTaskBounds) + } + private fun setUpFreeformTask( displayId: Int = DEFAULT_DISPLAY, - bounds: Rect? = null + bounds: Rect? = null, + active: Boolean = true ): RunningTaskInfo { val task = createFreeformTask(displayId, bounds) val activityInfo = ActivityInfo() task.topActivityInfo = activityInfo whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) - desktopModeTaskRepository.addActiveTask(displayId, task.taskId) - desktopModeTaskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId) + if (active) { + taskRepository.addActiveTask(displayId, task.taskId) + taskRepository.updateTaskVisibility(displayId, task.taskId, visible = true) + } + taskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId) runningTasks.add(task) return task } @@ -1628,46 +3271,56 @@ class DesktopTasksControllerTest : ShellTestCase() { } private fun setUpFullscreenTask( - displayId: Int = DEFAULT_DISPLAY, - isResizable: Boolean = true, - windowingMode: Int = WINDOWING_MODE_FULLSCREEN, - deviceOrientation: Int = ORIENTATION_LANDSCAPE, - screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED, - shouldLetterbox: Boolean = false + displayId: Int = DEFAULT_DISPLAY, + isResizable: Boolean = true, + windowingMode: Int = WINDOWING_MODE_FULLSCREEN, + deviceOrientation: Int = ORIENTATION_LANDSCAPE, + screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED, + shouldLetterbox: Boolean = false, + gravity: Int = Gravity.NO_GRAVITY, + enableUserFullscreenOverride: Boolean = false, + enableSystemFullscreenOverride: Boolean = false, + aspectRatioOverrideApplied: Boolean = false ): RunningTaskInfo { val task = createFullscreenTask(displayId) val activityInfo = ActivityInfo() activityInfo.screenOrientation = screenOrientation + activityInfo.windowLayout = ActivityInfo.WindowLayout(0, 0F, 0, 0F, gravity, 0, 0) with(task) { topActivityInfo = activityInfo isResizeable = isResizable configuration.orientation = deviceOrientation configuration.windowConfiguration.windowingMode = windowingMode + appCompatTaskInfo.isUserFullscreenOverrideEnabled = enableUserFullscreenOverride + appCompatTaskInfo.isSystemFullscreenOverrideEnabled = enableSystemFullscreenOverride + + if (deviceOrientation == ORIENTATION_LANDSCAPE) { + configuration.windowConfiguration.appBounds = + Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT) + appCompatTaskInfo.topActivityLetterboxAppWidth = DISPLAY_DIMENSION_LONG + appCompatTaskInfo.topActivityLetterboxAppHeight = DISPLAY_DIMENSION_SHORT + } else { + configuration.windowConfiguration.appBounds = + Rect(0, 0, DISPLAY_DIMENSION_SHORT, DISPLAY_DIMENSION_LONG) + appCompatTaskInfo.topActivityLetterboxAppWidth = DISPLAY_DIMENSION_SHORT + appCompatTaskInfo.topActivityLetterboxAppHeight = DISPLAY_DIMENSION_LONG + } if (shouldLetterbox) { + appCompatTaskInfo.setHasMinAspectRatioOverride(aspectRatioOverrideApplied) if (deviceOrientation == ORIENTATION_LANDSCAPE && screenOrientation == SCREEN_ORIENTATION_PORTRAIT) { // Letterbox to portrait size - appCompatTaskInfo.topActivityBoundsLetterboxed = true - appCompatTaskInfo.topActivityLetterboxWidth = 1200 - appCompatTaskInfo.topActivityLetterboxHeight = 1600 + appCompatTaskInfo.setTopActivityLetterboxed(true) + appCompatTaskInfo.topActivityLetterboxAppWidth = 1200 + appCompatTaskInfo.topActivityLetterboxAppHeight = 1600 } else if (deviceOrientation == ORIENTATION_PORTRAIT && screenOrientation == SCREEN_ORIENTATION_LANDSCAPE) { // Letterbox to landscape size - appCompatTaskInfo.topActivityBoundsLetterboxed = true - appCompatTaskInfo.topActivityLetterboxWidth = 1600 - appCompatTaskInfo.topActivityLetterboxHeight = 1200 + appCompatTaskInfo.setTopActivityLetterboxed(true) + appCompatTaskInfo.topActivityLetterboxAppWidth = 1600 + appCompatTaskInfo.topActivityLetterboxAppHeight = 1200 } - } else { - appCompatTaskInfo.topActivityBoundsLetterboxed = false - } - - if (deviceOrientation == ORIENTATION_LANDSCAPE) { - configuration.windowConfiguration.appBounds = - Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT) - } else { - configuration.windowConfiguration.appBounds = - Rect(0, 0, DISPLAY_DIMENSION_SHORT, DISPLAY_DIMENSION_LONG) } } whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) @@ -1678,11 +3331,23 @@ class DesktopTasksControllerTest : ShellTestCase() { private fun setUpLandscapeDisplay() { whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_LONG) whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_SHORT) + val stableBounds = Rect(0, 0, DISPLAY_DIMENSION_LONG, + DISPLAY_DIMENSION_SHORT - Companion.TASKBAR_FRAME_HEIGHT + ) + whenever(displayLayout.getStableBoundsForDesktopMode(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(stableBounds) + } } private fun setUpPortraitDisplay() { whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_SHORT) whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_LONG) + val stableBounds = Rect(0, 0, DISPLAY_DIMENSION_SHORT, + DISPLAY_DIMENSION_LONG - Companion.TASKBAR_FRAME_HEIGHT + ) + whenever(displayLayout.getStableBoundsForDesktopMode(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(stableBounds) + } } private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { @@ -1694,12 +3359,12 @@ class DesktopTasksControllerTest : ShellTestCase() { } private fun markTaskVisible(task: RunningTaskInfo) { - desktopModeTaskRepository.updateVisibleFreeformTasks( + taskRepository.updateTaskVisibility( task.displayId, task.taskId, visible = true) } private fun markTaskHidden(task: RunningTaskInfo) { - desktopModeTaskRepository.updateVisibleFreeformTasks( + taskRepository.updateTaskVisibility( task.displayId, task.taskId, visible = false) } @@ -1720,11 +3385,14 @@ class DesktopTasksControllerTest : ShellTestCase() { return arg.value } - private fun getLatestToggleResizeDesktopTaskWct(): WindowContainerTransaction { + private fun getLatestToggleResizeDesktopTaskWct( + currentBounds: Rect? = null + ): WindowContainerTransaction { val arg: ArgumentCaptor<WindowContainerTransaction> = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) if (ENABLE_SHELL_TRANSITIONS) { - verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce()).startTransition(capture(arg)) + verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce()) + .startTransition(capture(arg), eq(currentBounds)) } else { verify(shellTaskOrganizer).applyTransaction(capture(arg)) } @@ -1796,9 +3464,11 @@ class DesktopTasksControllerTest : ShellTestCase() { return TransitionRequestInfo(type, task, null /* remoteTransition */) } - companion object { + private companion object { const val SECOND_DISPLAY = 2 - private val STABLE_BOUNDS = Rect(0, 0, 1000, 1000) + val STABLE_BOUNDS = Rect(0, 0, 1000, 1000) + const val MAX_TASK_LIMIT = 6 + private const val TASKBAR_FRAME_HEIGHT = 200 } } @@ -1826,6 +3496,16 @@ private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: Runni } } +/** Checks if the reorder hierarchy operations in [range] correspond to [tasks] list */ +private fun WindowContainerTransaction.assertReorderSequenceInRange( + range: IntRange, + vararg tasks: RunningTaskInfo +) { + assertThat(hierarchyOps.slice(range).map { it.type to it.container }) + .containsExactlyElementsIn(tasks.map { HIERARCHY_OP_TYPE_REORDER to it.token.asBinder() }) + .inOrder() +} + private fun WindowContainerTransaction.assertRemoveAt(index: Int, token: WindowContainerToken) { assertIndexInBounds(index) val op = hierarchyOps[index] @@ -1863,6 +3543,14 @@ private fun WindowContainerTransaction?.anyDensityConfigChange( } ?: false } +private fun WindowContainerTransaction?.anyWindowingModeChange( + token: WindowContainerToken +): Boolean { +return this?.changes?.any { change -> + change.key == token.asBinder() && change.value.windowingMode >= 0 +} ?: false +} + private fun createTaskInfo(id: Int) = RecentTaskInfo().apply { taskId = id diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt index 77f917cc28d8..045e07796cb8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt @@ -18,25 +18,40 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RunningTaskInfo import android.os.Binder +import android.os.Handler import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_BACK import android.window.WindowContainerTransaction +import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_MINIMIZE_WINDOW +import com.android.internal.jank.InteractionJankMonitor import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask -import com.android.wm.shell.shared.DesktopModeStatus +import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.TransitionInfoBuilder import com.android.wm.shell.transition.Transitions import com.android.wm.shell.util.StubTransaction import com.google.common.truth.Truth.assertThat +import kotlin.test.assertFailsWith +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before import org.junit.Rule @@ -44,7 +59,10 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.any +import org.mockito.Mockito.spy import org.mockito.Mockito.`when` +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify import org.mockito.quality.Strictness @@ -55,6 +73,7 @@ import org.mockito.quality.Strictness */ @SmallTest @RunWith(AndroidTestingRunner::class) +@ExperimentalCoroutinesApi class DesktopTasksLimiterTest : ShellTestCase() { @JvmField @@ -63,34 +82,53 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer @Mock lateinit var transitions: Transitions + @Mock lateinit var interactionJankMonitor: InteractionJankMonitor + @Mock lateinit var handler: Handler + @Mock lateinit var testExecutor: ShellExecutor + @Mock lateinit var persistentRepository: DesktopPersistentRepository private lateinit var mockitoSession: StaticMockitoSession private lateinit var desktopTasksLimiter: DesktopTasksLimiter private lateinit var desktopTaskRepo: DesktopModeTaskRepository + private lateinit var shellInit: ShellInit + private lateinit var testScope: CoroutineScope @Before fun setUp() { mockitoSession = ExtendedMockito.mockitoSession().strictness(Strictness.LENIENT) .spyStatic(DesktopModeStatus::class.java).startMocking() doReturn(true).`when`{ DesktopModeStatus.canEnterDesktopMode(any()) } - - desktopTaskRepo = DesktopModeTaskRepository() - - desktopTasksLimiter = DesktopTasksLimiter( - transitions, desktopTaskRepo, shellTaskOrganizer) + shellInit = spy(ShellInit(testExecutor)) + Dispatchers.setMain(StandardTestDispatcher()) + testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + + desktopTaskRepo = + DesktopModeTaskRepository(context, shellInit, persistentRepository, testScope) + desktopTasksLimiter = + DesktopTasksLimiter(transitions, desktopTaskRepo, shellTaskOrganizer, MAX_TASK_LIMIT, + interactionJankMonitor, mContext, handler) } @After fun tearDown() { mockitoSession.finishMocking() + testScope.cancel() + } + + @Test + fun createDesktopTasksLimiter_withZeroLimit_shouldThrow() { + assertFailsWith<IllegalArgumentException> { + DesktopTasksLimiter(transitions, desktopTaskRepo, shellTaskOrganizer, 0, + interactionJankMonitor, mContext, handler) + } } - // Currently, the task limit can be overridden through an adb flag. This test ensures the limit - // hasn't been overridden. @Test - fun getMaxTaskLimit_isSameAsConstant() { - assertThat(desktopTasksLimiter.getMaxTaskLimit()).isEqualTo( - DesktopModeStatus.DEFAULT_MAX_TASK_LIMIT) + fun createDesktopTasksLimiter_withNegativeLimit_shouldThrow() { + assertFailsWith<IllegalArgumentException> { + DesktopTasksLimiter(transitions, desktopTaskRepo, shellTaskOrganizer, -5, + interactionJankMonitor, mContext, handler) + } } @Test @@ -205,9 +243,48 @@ class DesktopTasksLimiterTest : ShellTestCase() { } @Test + fun removeLeftoverMinimizedTasks_activeNonMinimizedTasksStillAround_doesNothing() { + desktopTaskRepo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 1) + desktopTaskRepo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 2) + desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = 2) + + val wct = WindowContainerTransaction() + desktopTasksLimiter.leftoverMinimizedTasksRemover.removeLeftoverMinimizedTasks( + DEFAULT_DISPLAY, wct) + + assertThat(wct.isEmpty).isTrue() + } + + @Test + fun removeLeftoverMinimizedTasks_noMinimizedTasks_doesNothing() { + val wct = WindowContainerTransaction() + desktopTasksLimiter.leftoverMinimizedTasksRemover.removeLeftoverMinimizedTasks( + DEFAULT_DISPLAY, wct) + + assertThat(wct.isEmpty).isTrue() + } + + @Test + fun removeLeftoverMinimizedTasks_onlyMinimizedTasksLeft_removesAllMinimizedTasks() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task1.taskId) + desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + + val wct = WindowContainerTransaction() + desktopTasksLimiter.leftoverMinimizedTasksRemover.removeLeftoverMinimizedTasks( + DEFAULT_DISPLAY, wct) + + assertThat(wct.hierarchyOps).hasSize(2) + assertThat(wct.hierarchyOps[0].type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK) + assertThat(wct.hierarchyOps[0].container).isEqualTo(task1.token.asBinder()) + assertThat(wct.hierarchyOps[1].type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK) + assertThat(wct.hierarchyOps[1].container).isEqualTo(task2.token.asBinder()) + } + + @Test fun addAndGetMinimizeTaskChangesIfNeeded_tasksWithinLimit_noTaskMinimized() { - val taskLimit = desktopTasksLimiter.getMaxTaskLimit() - (1..<taskLimit).forEach { _ -> setUpFreeformTask() } + (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() } val wct = WindowContainerTransaction() val minimizedTaskId = @@ -222,9 +299,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun addAndGetMinimizeTaskChangesIfNeeded_tasksAboveLimit_backTaskMinimized() { - val taskLimit = desktopTasksLimiter.getMaxTaskLimit() // The following list will be ordered bottom -> top, as the last task is moved to top last. - val tasks = (1..taskLimit).map { setUpFreeformTask() } + val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } val wct = WindowContainerTransaction() val minimizedTaskId = @@ -241,8 +317,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun addAndGetMinimizeTaskChangesIfNeeded_nonMinimizedTasksWithinLimit_noTaskMinimized() { - val taskLimit = desktopTasksLimiter.getMaxTaskLimit() - val tasks = (1..taskLimit).map { setUpFreeformTask() } + val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = tasks[0].taskId) val wct = WindowContainerTransaction() @@ -258,8 +333,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getTaskToMinimizeIfNeeded_tasksWithinLimit_returnsNull() { - val taskLimit = desktopTasksLimiter.getMaxTaskLimit() - val tasks = (1..taskLimit).map { setUpFreeformTask() } + val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded( visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId }) @@ -269,8 +343,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getTaskToMinimizeIfNeeded_tasksAboveLimit_returnsBackTask() { - val taskLimit = desktopTasksLimiter.getMaxTaskLimit() - val tasks = (1..taskLimit + 1).map { setUpFreeformTask() } + val tasks = (1..MAX_TASK_LIMIT + 1).map { setUpFreeformTask() } val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded( visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId }) @@ -280,9 +353,22 @@ class DesktopTasksLimiterTest : ShellTestCase() { } @Test + fun getTaskToMinimizeIfNeeded_tasksAboveLimit_otherLimit_returnsBackTask() { + desktopTasksLimiter = + DesktopTasksLimiter(transitions, desktopTaskRepo, shellTaskOrganizer, MAX_TASK_LIMIT2, + interactionJankMonitor, mContext, handler) + val tasks = (1..MAX_TASK_LIMIT2 + 1).map { setUpFreeformTask() } + + val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded( + visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId }) + + // first == front, last == back + assertThat(minimizedTask).isEqualTo(tasks.last()) + } + + @Test fun getTaskToMinimizeIfNeeded_withNewTask_tasksAboveLimit_returnsBackTask() { - val taskLimit = desktopTasksLimiter.getMaxTaskLimit() - val tasks = (1..taskLimit).map { setUpFreeformTask() } + val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded( visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId }, @@ -292,6 +378,95 @@ class DesktopTasksLimiterTest : ShellTestCase() { assertThat(minimizedTask).isEqualTo(tasks.last()) } + @Test + fun minimizeTransitionReadyAndFinished_logsJankInstrumentationBeginAndEnd() { + (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() } + val transition = Binder() + val task = setUpFreeformTask() + desktopTasksLimiter.addPendingMinimizeChange( + transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + transition, + TransitionInfoBuilder(TRANSIT_OPEN).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + desktopTasksLimiter.getTransitionObserver().onTransitionStarting(transition) + + verify(interactionJankMonitor).begin( + any(), + eq(mContext), + eq(handler), + eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)) + + desktopTasksLimiter.getTransitionObserver().onTransitionFinished( + transition, + /* aborted = */ false) + + verify(interactionJankMonitor).end(eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)) + } + + @Test + fun minimizeTransitionReadyAndAborted_logsJankInstrumentationBeginAndCancel() { + (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() } + val transition = Binder() + val task = setUpFreeformTask() + desktopTasksLimiter.addPendingMinimizeChange( + transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + transition, + TransitionInfoBuilder(TRANSIT_OPEN).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + desktopTasksLimiter.getTransitionObserver().onTransitionStarting(transition) + + verify(interactionJankMonitor).begin( + any(), + eq(mContext), + eq(handler), + eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW), + ) + + desktopTasksLimiter.getTransitionObserver().onTransitionFinished( + transition, + /* aborted = */ true) + + verify(interactionJankMonitor).cancel(eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)) + } + + @Test + fun minimizeTransitionReadyAndMerged_logsJankInstrumentationBeginAndEnd() { + (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() } + val mergedTransition = Binder() + val newTransition = Binder() + val task = setUpFreeformTask() + desktopTasksLimiter.addPendingMinimizeChange( + mergedTransition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + mergedTransition, + TransitionInfoBuilder(TRANSIT_OPEN).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + desktopTasksLimiter.getTransitionObserver().onTransitionStarting(mergedTransition) + + verify(interactionJankMonitor).begin( + any(), + eq(mContext), + eq(handler), + eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)) + + desktopTasksLimiter.getTransitionObserver().onTransitionMerged( + mergedTransition, + newTransition) + + verify(interactionJankMonitor).end(eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)) + } + private fun setUpFreeformTask( displayId: Int = DEFAULT_DISPLAY, ): RunningTaskInfo { @@ -303,7 +478,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { } private fun markTaskVisible(task: RunningTaskInfo) { - desktopTaskRepo.updateVisibleFreeformTasks( + desktopTaskRepo.updateTaskVisibility( task.displayId, task.taskId, visible = true @@ -311,10 +486,15 @@ class DesktopTasksLimiterTest : ShellTestCase() { } private fun markTaskHidden(task: RunningTaskInfo) { - desktopTaskRepo.updateVisibleFreeformTasks( + desktopTaskRepo.updateTaskVisibility( task.displayId, task.taskId, visible = false ) } + + private companion object { + const val MAX_TASK_LIMIT = 6 + const val MAX_TASK_LIMIT2 = 9 + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index bbf523bc40d2..d9387d2f08dd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -8,38 +8,50 @@ import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.app.WindowConfiguration.WindowingMode import android.graphics.PointF import android.os.IBinder +import android.os.SystemProperties import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.SurfaceControl +import android.view.WindowManager.TRANSIT_OPEN import android.window.TransitionInfo import android.window.TransitionInfo.FLAG_IS_WALLPAPER import android.window.WindowContainerTransaction import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD +import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE +import com.android.internal.jank.InteractionJankMonitor import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder -import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT -import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP import com.android.wm.shell.windowdecor.MoveToDesktopAnimator +import java.util.function.Supplier +import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Mock +import org.mockito.MockitoSession import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyZeroInteractions import org.mockito.kotlin.whenever -import java.util.function.Supplier +import org.mockito.quality.Strictness /** Tests of [DragToDesktopTransitionHandler]. */ @SmallTest @@ -51,31 +63,56 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock private lateinit var splitScreenController: SplitScreenController @Mock private lateinit var dragAnimator: MoveToDesktopAnimator + @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor + @Mock private lateinit var draggedTaskLeash: SurfaceControl + @Mock private lateinit var homeTaskLeash: SurfaceControl private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() } - private lateinit var handler: DragToDesktopTransitionHandler + private lateinit var defaultHandler: DragToDesktopTransitionHandler + private lateinit var springHandler: SpringDragToDesktopTransitionHandler + private lateinit var mockitoSession: MockitoSession @Before fun setUp() { - handler = - DragToDesktopTransitionHandler( + defaultHandler = + DefaultDragToDesktopTransitionHandler( + context, + transitions, + taskDisplayAreaOrganizer, + mockInteractionJankMonitor, + transactionSupplier, + ) + .apply { setSplitScreenController(splitScreenController) } + springHandler = + SpringDragToDesktopTransitionHandler( context, transitions, taskDisplayAreaOrganizer, - transactionSupplier + mockInteractionJankMonitor, + transactionSupplier, ) .apply { setSplitScreenController(splitScreenController) } + mockitoSession = + ExtendedMockito.mockitoSession() + .strictness(Strictness.LENIENT) + .mockStatic(SystemProperties::class.java) + .startMocking() + } + + @After + fun tearDown() { + mockitoSession.finishMocking() } @Test fun startDragToDesktop_animateDragWhenReady() { val task = createTask() // Simulate transition is started. - val transition = startDragToDesktopTransition(task, dragAnimator) + val transition = startDragToDesktopTransition(defaultHandler, task, dragAnimator) // Now it's ready to animate. - handler.startAnimation( + defaultHandler.startAnimation( transition = transition, info = createTransitionInfo( @@ -92,65 +129,70 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun startDragToDesktop_cancelledBeforeReady_startCancelTransition() { - performEarlyCancel(DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL) + performEarlyCancel( + defaultHandler, + DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL + ) verify(transitions) - .startTransition(eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), any(), eq(handler)) + .startTransition( + eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), + any(), + eq(defaultHandler) + ) } @Test fun startDragToDesktop_cancelledBeforeReady_verifySplitLeftCancel() { - performEarlyCancel(DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT) - verify(splitScreenController).requestEnterSplitSelect( - any(), - any(), - eq(SPLIT_POSITION_TOP_OR_LEFT), - any() + performEarlyCancel( + defaultHandler, + DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT ) + verify(splitScreenController) + .requestEnterSplitSelect(any(), any(), eq(SPLIT_POSITION_TOP_OR_LEFT), any()) } @Test fun startDragToDesktop_cancelledBeforeReady_verifySplitRightCancel() { - performEarlyCancel(DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT) - verify(splitScreenController).requestEnterSplitSelect( - any(), - any(), - eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), - any() + performEarlyCancel( + defaultHandler, + DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT ) + verify(splitScreenController) + .requestEnterSplitSelect(any(), any(), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()) } @Test fun startDragToDesktop_aborted_finishDropped() { val task = createTask() // Simulate transition is started. - val transition = startDragToDesktopTransition(task, dragAnimator) + val transition = startDragToDesktopTransition(defaultHandler, task, dragAnimator) // But the transition was aborted. - handler.onTransitionConsumed(transition, aborted = true, mock()) + defaultHandler.onTransitionConsumed(transition, aborted = true, mock()) // Attempt to finish the failed drag start. - handler.finishDragToDesktopTransition(WindowContainerTransaction()) + defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction()) // Should not be attempted and state should be reset. verify(transitions, never()) - .startTransition(eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP), any(), any()) - assertFalse(handler.inProgress) + .startTransition(eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP), any(), any()) + assertFalse(defaultHandler.inProgress) } @Test fun startDragToDesktop_aborted_cancelDropped() { val task = createTask() // Simulate transition is started. - val transition = startDragToDesktopTransition(task, dragAnimator) + val transition = startDragToDesktopTransition(defaultHandler, task, dragAnimator) // But the transition was aborted. - handler.onTransitionConsumed(transition, aborted = true, mock()) + defaultHandler.onTransitionConsumed(transition, aborted = true, mock()) // Attempt to finish the failed drag start. - handler.cancelDragToDesktopTransition( + defaultHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL ) // Should not be attempted and state should be reset. - assertFalse(handler.inProgress) + assertFalse(defaultHandler.inProgress) } @Test @@ -158,23 +200,78 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { val task = createTask() // Simulate attempt to start two drag to desktop transitions. - startDragToDesktopTransition(task, dragAnimator) - startDragToDesktopTransition(task, dragAnimator) + startDragToDesktopTransition(defaultHandler, task, dragAnimator) + startDragToDesktopTransition(defaultHandler, task, dragAnimator) // Verify transition only started once. - verify(transitions, times(1)).startTransition( + verify(transitions, times(1)) + .startTransition( eq(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP), any(), - eq(handler) - ) + eq(defaultHandler) + ) + } + + @Test + fun isHomeChange_withoutTaskInfo_returnsFalse() { + val change = + TransitionInfo.Change(mock(), homeTaskLeash).apply { + parent = null + taskInfo = null + } + + assertFalse(defaultHandler.isHomeChange(change)) + assertFalse(springHandler.isHomeChange(change)) + } + + @Test + fun isHomeChange_withStandardActivityTaskInfo_returnsFalse() { + val change = + TransitionInfo.Change(mock(), homeTaskLeash).apply { + parent = null + taskInfo = + TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_STANDARD).build() + } + + assertFalse(defaultHandler.isHomeChange(change)) + assertFalse(springHandler.isHomeChange(change)) + } + + @Test + fun isHomeChange_withHomeActivityTaskInfo_returnsTrue() { + val change = + TransitionInfo.Change(mock(), homeTaskLeash).apply { + parent = null + taskInfo = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() + } + + assertTrue(defaultHandler.isHomeChange(change)) + assertTrue(springHandler.isHomeChange(change)) + } + + @Test + fun isHomeChange_withSingleTranslucentHomeActivityTaskInfo_returnsFalse() { + val change = + TransitionInfo.Change(mock(), homeTaskLeash).apply { + parent = null + taskInfo = + TestRunningTaskInfoBuilder() + .setActivityType(ACTIVITY_TYPE_HOME) + .setTopActivityTransparent(true) + .setNumActivities(1) + .build() + } + + assertFalse(defaultHandler.isHomeChange(change)) + assertFalse(springHandler.isHomeChange(change)) } @Test fun cancelDragToDesktop_startWasReady_cancel() { - startDrag() + startDrag(defaultHandler) // Then user cancelled after it had already started. - handler.cancelDragToDesktopTransition( + defaultHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL ) @@ -184,48 +281,40 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun cancelDragToDesktop_splitLeftCancelType_splitRequested() { - startDrag() + startDrag(defaultHandler) // Then user cancelled it, requesting split. - handler.cancelDragToDesktopTransition( + defaultHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT ) // Verify the request went through split controller. - verify(splitScreenController).requestEnterSplitSelect( - any(), - any(), - eq(SPLIT_POSITION_TOP_OR_LEFT), - any() - ) + verify(splitScreenController) + .requestEnterSplitSelect(any(), any(), eq(SPLIT_POSITION_TOP_OR_LEFT), any()) } @Test fun cancelDragToDesktop_splitRightCancelType_splitRequested() { - startDrag() + startDrag(defaultHandler) // Then user cancelled it, requesting split. - handler.cancelDragToDesktopTransition( + defaultHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT ) // Verify the request went through split controller. - verify(splitScreenController).requestEnterSplitSelect( - any(), - any(), - eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), - any() - ) + verify(splitScreenController) + .requestEnterSplitSelect(any(), any(), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()) } @Test fun cancelDragToDesktop_startWasNotReady_animateCancel() { val task = createTask() // Simulate transition is started and is ready to animate. - startDragToDesktopTransition(task, dragAnimator) + startDragToDesktopTransition(defaultHandler, task, dragAnimator) // Then user cancelled before the transition was ready and animated. - handler.cancelDragToDesktopTransition( + defaultHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL ) @@ -236,50 +325,250 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun cancelDragToDesktop_transitionNotInProgress_dropCancel() { // Then cancel is called before the transition was started. - handler.cancelDragToDesktopTransition( + defaultHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL ) // Verify cancel is dropped. - verify(transitions, never()).startTransition( + verify(transitions, never()) + .startTransition( eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), any(), - eq(handler) - ) + eq(defaultHandler) + ) } @Test fun finishDragToDesktop_transitionNotInProgress_dropFinish() { // Then finish is called before the transition was started. - handler.finishDragToDesktopTransition(WindowContainerTransaction()) + defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction()) // Verify finish is dropped. - verify(transitions, never()).startTransition( + verify(transitions, never()) + .startTransition( eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP), any(), - eq(handler) + eq(defaultHandler) + ) + } + + @Test + fun mergeAnimation_otherTransition_doesNotMerge() { + val transaction = mock<SurfaceControl.Transaction>() + val finishCallback = mock<Transitions.TransitionFinishCallback>() + val task = createTask() + + startDrag(defaultHandler, task) + defaultHandler.mergeAnimation( + transition = mock(), + info = createTransitionInfo(type = TRANSIT_OPEN, draggedTask = task), + t = transaction, + mergeTarget = mock(), + finishCallback = finishCallback + ) + + // Should NOT have any transaction changes + verifyZeroInteractions(transaction) + // Should NOT merge animation + verify(finishCallback, never()).onTransitionFinished(any()) + } + + @Test + fun mergeAnimation_endTransition_mergesAnimation() { + val playingFinishTransaction = mock<SurfaceControl.Transaction>() + val mergedStartTransaction = mock<SurfaceControl.Transaction>() + val finishCallback = mock<Transitions.TransitionFinishCallback>() + val task = createTask() + val startTransition = + startDrag(defaultHandler, task, finishTransaction = playingFinishTransaction) + defaultHandler.onTaskResizeAnimationListener = mock() + + defaultHandler.mergeAnimation( + transition = mock(), + info = + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + draggedTask = task + ), + t = mergedStartTransaction, + mergeTarget = startTransition, + finishCallback = finishCallback ) + + // Should show dragged task layer in start and finish transaction + verify(mergedStartTransaction).show(draggedTaskLeash) + verify(playingFinishTransaction).show(draggedTaskLeash) + // Should update the dragged task layer + verify(mergedStartTransaction).setLayer(eq(draggedTaskLeash), anyInt()) + // Should merge animation + verify(finishCallback).onTransitionFinished(null) + } + + @Test + fun mergeAnimation_endTransition_springHandler_hidesHome() { + whenever(dragAnimator.computeCurrentVelocity()).thenReturn(PointF()) + val playingFinishTransaction = mock<SurfaceControl.Transaction>() + val mergedStartTransaction = mock<SurfaceControl.Transaction>() + val finishCallback = mock<Transitions.TransitionFinishCallback>() + val task = createTask() + val startTransition = + startDrag(springHandler, task, finishTransaction = playingFinishTransaction) + springHandler.onTaskResizeAnimationListener = mock() + + springHandler.mergeAnimation( + transition = mock(), + info = + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + draggedTask = task + ), + t = mergedStartTransaction, + mergeTarget = startTransition, + finishCallback = finishCallback + ) + + // Should show dragged task layer in start and finish transaction + verify(mergedStartTransaction).show(draggedTaskLeash) + verify(playingFinishTransaction).show(draggedTaskLeash) + // Should update the dragged task layer + verify(mergedStartTransaction).setLayer(eq(draggedTaskLeash), anyInt()) + // Should hide home task leash in finish transaction + verify(playingFinishTransaction).hide(homeTaskLeash) + // Should merge animation + verify(finishCallback).onTransitionFinished(null) + } + + @Test + fun propertyValue_returnsSystemPropertyValue() { + val name = "property_name" + val value = 10f + + whenever(SystemProperties.getInt(eq(systemPropertiesKey(name)), anyInt())) + .thenReturn(value.toInt()) + + assertEquals( + "Expects to return system properties stored value", + /* expected= */ value, + /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue(name) + ) + } + + @Test + fun propertyValue_withScale_returnsScaledSystemPropertyValue() { + val name = "property_name" + val value = 10f + val scale = 100f + + whenever(SystemProperties.getInt(eq(systemPropertiesKey(name)), anyInt())) + .thenReturn(value.toInt()) + + assertEquals( + "Expects to return scaled system properties stored value", + /* expected= */ value / scale, + /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue(name, scale = scale) + ) + } + + @Test + fun propertyValue_notSet_returnsDefaultValue() { + val name = "property_name" + val defaultValue = 50f + + whenever(SystemProperties.getInt(eq(systemPropertiesKey(name)), eq(defaultValue.toInt()))) + .thenReturn(defaultValue.toInt()) + + assertEquals( + "Expects to return the default value", + /* expected= */ defaultValue, + /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue( + name, + default = defaultValue + ) + ) + } + + @Test + fun propertyValue_withScaleNotSet_returnsDefaultValue() { + val name = "property_name" + val defaultValue = 0.5f + val scale = 100f + // Default value is multiplied when provided as a default value for [SystemProperties] + val scaledDefault = (defaultValue * scale).toInt() + + whenever(SystemProperties.getInt(eq(systemPropertiesKey(name)), eq(scaledDefault))) + .thenReturn(scaledDefault) + + assertEquals( + "Expects to return the default value", + /* expected= */ defaultValue, + /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue( + name, + default = defaultValue, + scale = scale + ) + ) + } + + @Test + fun startDragToDesktop_aborted_logsDragHoldCancelled() { + val transition = startDragToDesktopTransition(defaultHandler, createTask(), dragAnimator) + + defaultHandler.onTransitionConsumed(transition, aborted = true, mock()) + + verify(mockInteractionJankMonitor).cancel(eq(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)) + verify(mockInteractionJankMonitor, times(0)).cancel( + eq(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE)) } - private fun startDrag() { + @Test + fun mergeEndDragToDesktop_aborted_logsDragReleaseCancelled() { val task = createTask() + val startTransition = startDrag(defaultHandler, task) + val endTransition = mock<IBinder>() + defaultHandler.onTaskResizeAnimationListener = mock() + defaultHandler.mergeAnimation( + transition = endTransition, + info = createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + draggedTask = task + ), + t = mock<SurfaceControl.Transaction>(), + mergeTarget = startTransition, + finishCallback = mock<Transitions.TransitionFinishCallback>() + ) + + defaultHandler.onTransitionConsumed(endTransition, aborted = true, mock()) + + verify(mockInteractionJankMonitor) + .cancel(eq(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE)) + verify(mockInteractionJankMonitor, times(0)) + .cancel(eq(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)) + } + + private fun startDrag( + handler: DragToDesktopTransitionHandler, + task: RunningTaskInfo = createTask(), + finishTransaction: SurfaceControl.Transaction = mock() + ): IBinder { whenever(dragAnimator.position).thenReturn(PointF()) // Simulate transition is started and is ready to animate. - val transition = startDragToDesktopTransition(task, dragAnimator) + val transition = startDragToDesktopTransition(handler, task, dragAnimator) handler.startAnimation( transition = transition, info = - createTransitionInfo( - type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, - draggedTask = task - ), + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, + draggedTask = task + ), startTransaction = mock(), - finishTransaction = mock(), + finishTransaction = finishTransaction, finishCallback = {} ) + return transition } private fun startDragToDesktopTransition( + handler: DragToDesktopTransitionHandler, task: RunningTaskInfo, dragAnimator: MoveToDesktopAnimator ): IBinder { @@ -296,20 +585,23 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { return token } - private fun performEarlyCancel(cancelState: DragToDesktopTransitionHandler.CancelState) { + private fun performEarlyCancel( + handler: DragToDesktopTransitionHandler, + cancelState: DragToDesktopTransitionHandler.CancelState + ) { val task = createTask() // Simulate transition is started and is ready to animate. - val transition = startDragToDesktopTransition(task, dragAnimator) + val transition = startDragToDesktopTransition(handler, task, dragAnimator) handler.cancelDragToDesktopTransition(cancelState) handler.startAnimation( transition = transition, info = - createTransitionInfo( - type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, - draggedTask = task - ), + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, + draggedTask = task + ), startTransaction = mock(), finishTransaction = mock(), finishCallback = {} @@ -336,7 +628,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo): TransitionInfo { return TransitionInfo(type, 0 /* flags */).apply { addChange( // Home. - TransitionInfo.Change(mock(), mock()).apply { + TransitionInfo.Change(mock(), homeTaskLeash).apply { parent = null taskInfo = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() @@ -344,7 +636,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } ) addChange( // Dragged Task. - TransitionInfo.Change(mock(), mock()).apply { + TransitionInfo.Change(mock(), draggedTaskLeash).apply { parent = null taskInfo = draggedTask } @@ -358,4 +650,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { ) } } + + private fun systemPropertiesKey(name: String) = + "${SpringDragToDesktopTransitionHandler.SYSTEM_PROPERTIES_GROUP}.$name" } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java index b2467e9a62cf..fefa933c5208 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java @@ -34,6 +34,7 @@ import android.app.WindowConfiguration; import android.content.Context; import android.content.res.Resources; import android.graphics.Point; +import android.os.Handler; import android.os.IBinder; import android.util.DisplayMetrics; import android.view.SurfaceControl; @@ -45,9 +46,10 @@ import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; +import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.transition.Transitions; import org.junit.Before; @@ -65,6 +67,8 @@ public class ExitDesktopTaskTransitionHandlerTest extends ShellTestCase { @Mock private Transitions mTransitions; @Mock + private InteractionJankMonitor mInteractionJankMonitor; + @Mock IBinder mToken; @Mock Supplier<SurfaceControl.Transaction> mTransactionFactory; @@ -78,6 +82,8 @@ public class ExitDesktopTaskTransitionHandlerTest extends ShellTestCase { Transitions.TransitionFinishCallback mTransitionFinishCallback; @Mock ShellExecutor mExecutor; + @Mock + Handler mHandler; private Point mPoint; private ExitDesktopTaskTransitionHandler mExitDesktopTaskTransitionHandler; @@ -94,7 +100,7 @@ public class ExitDesktopTaskTransitionHandlerTest extends ShellTestCase { .thenReturn(getContext().getResources().getDisplayMetrics()); mExitDesktopTaskTransitionHandler = new ExitDesktopTaskTransitionHandler(mTransitions, - mContext); + mContext, mInteractionJankMonitor, mHandler); mPoint = new Point(0, 0); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/OWNERS new file mode 100644 index 000000000000..553540cbb86c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 929241 +# includes OWNERS from parent directories
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt new file mode 100644 index 000000000000..e3caf2ede99d --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2024 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.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class WindowDecorCaptionHandleRepositoryTest { + private lateinit var captionHandleRepository: WindowDecorCaptionHandleRepository + + @Before + fun setUp() { + captionHandleRepository = WindowDecorCaptionHandleRepository() + } + + @Test + fun initialState_noAction_returnsNoCaption() { + // Check the initial value of `captionStateFlow`. + assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(CaptionState.NoCaption) + } + + @Test + fun notifyCaptionChange_toAppHandleVisible_updatesStateWithCorrectData() { + val taskInfo = createTaskInfo(WINDOWING_MODE_FULLSCREEN, GMAIL_PACKAGE_NAME) + val appHandleCaptionState = + CaptionState.AppHandle( + taskInfo, false, Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3)) + + captionHandleRepository.notifyCaptionChanged(appHandleCaptionState) + + assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(appHandleCaptionState) + } + + @Test + fun notifyCaptionChange_toAppChipVisible_updatesStateWithCorrectData() { + val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM, GMAIL_PACKAGE_NAME) + val appHeaderCaptionState = + CaptionState.AppHeader( + taskInfo, true, Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3)) + + captionHandleRepository.notifyCaptionChanged(appHeaderCaptionState) + + assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(appHeaderCaptionState) + } + + @Test + fun notifyCaptionChange_toNoCaption_updatesState() { + captionHandleRepository.notifyCaptionChanged(CaptionState.NoCaption) + + assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(CaptionState.NoCaption) + } + + private fun createTaskInfo( + deviceWindowingMode: Int = WINDOWING_MODE_UNDEFINED, + runningTaskPackageName: String = LAUNCHER_PACKAGE_NAME + ): RunningTaskInfo = + RunningTaskInfo().apply { + configuration.windowConfiguration.apply { windowingMode = deviceWindowingMode } + topActivityInfo?.apply { packageName = runningTaskPackageName } + } + + private companion object { + const val GMAIL_PACKAGE_NAME = "com.google.android.gm" + const val LAUNCHER_PACKAGE_NAME = "com.google.android.apps.nexuslauncher" + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt new file mode 100644 index 000000000000..765021fbbd3d --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2024 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.desktopmode.education + +import android.content.Context +import android.testing.AndroidTestingRunner +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import androidx.test.core.app.ApplicationProvider +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository +import com.android.wm.shell.desktopmode.education.data.WindowingEducationProto +import com.android.wm.shell.util.createWindowingEducationProto +import com.google.common.truth.Truth.assertThat +import java.io.File +import java.time.Duration +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@ExperimentalCoroutinesApi +class AppHandleEducationDatastoreRepositoryTest { + private val testContext: Context = InstrumentationRegistry.getInstrumentation().targetContext + private lateinit var testDatastore: DataStore<WindowingEducationProto> + private lateinit var datastoreRepository: AppHandleEducationDatastoreRepository + private lateinit var datastoreScope: CoroutineScope + + @Before + fun setUp() { + Dispatchers.setMain(StandardTestDispatcher()) + datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + testDatastore = + DataStoreFactory.create( + serializer = + AppHandleEducationDatastoreRepository.Companion.WindowingEducationProtoSerializer, + scope = datastoreScope) { + testContext.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_TEST_FILE) + } + datastoreRepository = AppHandleEducationDatastoreRepository(testDatastore) + } + + @After + fun tearDown() { + File(ApplicationProvider.getApplicationContext<Context>().filesDir, "datastore") + .deleteRecursively() + + datastoreScope.cancel() + } + + @Test + fun getWindowingEducationProto_returnsCorrectProto() = + runTest(StandardTestDispatcher()) { + val windowingEducationProto = + createWindowingEducationProto( + educationViewedTimestampMillis = 123L, + featureUsedTimestampMillis = 124L, + appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 2), + appUsageStatsLastUpdateTimestampMillis = 125L) + testDatastore.updateData { windowingEducationProto } + + val resultProto = datastoreRepository.windowingEducationProto() + + assertThat(resultProto).isEqualTo(windowingEducationProto) + } + + @Test + fun updateAppUsageStats_updatesDatastoreProto() = + runTest(StandardTestDispatcher()) { + val appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 3) + val appUsageStatsLastUpdateTimestamp = Duration.ofMillis(123L) + val windowingEducationProto = + createWindowingEducationProto( + appUsageStats = appUsageStats, + appUsageStatsLastUpdateTimestampMillis = + appUsageStatsLastUpdateTimestamp.toMillis()) + + datastoreRepository.updateAppUsageStats(appUsageStats, appUsageStatsLastUpdateTimestamp) + + val result = testDatastore.data.first() + assertThat(result).isEqualTo(windowingEducationProto) + } + + companion object { + private const val GMAIL_PACKAGE_NAME = "com.google.android.gm" + private const val APP_HANDLE_EDUCATION_DATASTORE_TEST_FILE = "app_handle_education_test.pb" + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt new file mode 100644 index 000000000000..c0d71c0bf5db --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2024 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.desktopmode.education + +import android.app.usage.UsageStats +import android.app.usage.UsageStatsManager +import android.content.Context +import android.testing.AndroidTestingRunner +import android.testing.TestableContext +import android.testing.TestableResources +import androidx.test.filters.SmallTest +import com.android.wm.shell.R +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository +import com.android.wm.shell.util.createWindowingEducationProto +import com.google.common.truth.Truth.assertThat +import kotlin.Int.Companion.MAX_VALUE +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class AppHandleEducationFilterTest : ShellTestCase() { + @Mock private lateinit var datastoreRepository: AppHandleEducationDatastoreRepository + @Mock private lateinit var mockUsageStatsManager: UsageStatsManager + private lateinit var educationFilter: AppHandleEducationFilter + private lateinit var testableResources: TestableResources + private lateinit var testableContext: TestableContext + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + testableContext = TestableContext(mContext) + testableResources = + testableContext.orCreateTestableResources.apply { + addOverride( + R.array.desktop_windowing_app_handle_education_allowlist_apps, + arrayOf(GMAIL_PACKAGE_NAME)) + addOverride(R.integer.desktop_windowing_education_required_time_since_setup_seconds, 0) + addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3) + addOverride( + R.integer.desktop_windowing_education_app_usage_cache_interval_seconds, MAX_VALUE) + addOverride(R.integer.desktop_windowing_education_app_launch_interval_seconds, 100) + } + testableContext.addMockSystemService(Context.USAGE_STATS_SERVICE, mockUsageStatsManager) + educationFilter = AppHandleEducationFilter(testableContext, datastoreRepository) + } + + @Test + fun shouldShowAppHandleEducation_isTriggerValid_returnsTrue() = runTest { + // setup() makes sure that all of the conditions satisfy and #shouldShowAppHandleEducation + // should return true + val windowingEducationProto = + createWindowingEducationProto( + appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), + appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE) + `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) + + val result = educationFilter.shouldShowAppHandleEducation(GMAIL_PACKAGE_NAME) + + assertThat(result).isTrue() + } + + @Test + fun shouldShowAppHandleEducation_focusAppNotInAllowlist_returnsFalse() = runTest { + // Pass Youtube as current focus app, it is not in allowlist hence #shouldShowAppHandleEducation + // should return false + testableResources.addOverride( + R.array.desktop_windowing_app_handle_education_allowlist_apps, arrayOf(GMAIL_PACKAGE_NAME)) + val windowingEducationProto = + createWindowingEducationProto( + appUsageStats = mapOf(YOUTUBE_PACKAGE_NAME to 4), + appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE) + `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) + + val result = educationFilter.shouldShowAppHandleEducation(YOUTUBE_PACKAGE_NAME) + + assertThat(result).isFalse() + } + + @Test + fun shouldShowAppHandleEducation_timeSinceSetupIsNotSufficient_returnsFalse() = runTest { + // Time required to have passed setup is > 100 years, hence #shouldShowAppHandleEducation should + // return false + testableResources.addOverride( + R.integer.desktop_windowing_education_required_time_since_setup_seconds, MAX_VALUE) + val windowingEducationProto = + createWindowingEducationProto( + appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), + appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE) + `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) + + val result = educationFilter.shouldShowAppHandleEducation(GMAIL_PACKAGE_NAME) + + assertThat(result).isFalse() + } + + @Test + fun shouldShowAppHandleEducation_educationViewedBefore_returnsFalse() = runTest { + // Education has been viewed before, hence #shouldShowAppHandleEducation should return false + val windowingEducationProto = + createWindowingEducationProto( + appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), + educationViewedTimestampMillis = 123L, + appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE) + `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) + + val result = educationFilter.shouldShowAppHandleEducation(GMAIL_PACKAGE_NAME) + + assertThat(result).isFalse() + } + + @Test + fun shouldShowAppHandleEducation_featureUsedBefore_returnsFalse() = runTest { + // Feature has been used before, hence #shouldShowAppHandleEducation should return false + val windowingEducationProto = + createWindowingEducationProto( + appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), + featureUsedTimestampMillis = 123L, + appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE) + `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) + + val result = educationFilter.shouldShowAppHandleEducation(GMAIL_PACKAGE_NAME) + + assertThat(result).isFalse() + } + + @Test + fun shouldShowAppHandleEducation_doesNotHaveMinAppUsage_returnsFalse() = runTest { + // Simulate that gmail app has been launched twice before, minimum app launch count is 3, hence + // #shouldShowAppHandleEducation should return false + testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3) + val windowingEducationProto = + createWindowingEducationProto( + appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 2), + appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE) + `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) + + val result = educationFilter.shouldShowAppHandleEducation(GMAIL_PACKAGE_NAME) + + assertThat(result).isFalse() + } + + @Test + fun shouldShowAppHandleEducation_appUsageStatsStale_queryAppUsageStats() = runTest { + // UsageStats caching interval is set to 0ms, that means caching should happen very frequently + testableResources.addOverride( + R.integer.desktop_windowing_education_app_usage_cache_interval_seconds, 0) + // The DataStore currently holds a proto object where Gmail's app launch count is recorded as 4. + // This value exceeds the minimum required count of 3. + testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3) + val windowingEducationProto = + createWindowingEducationProto( + appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), + appUsageStatsLastUpdateTimestampMillis = 0) + // The mocked UsageStatsManager is configured to return a launch count of 2 for Gmail. + // This value is below the minimum required count of 3. + `when`(mockUsageStatsManager.queryAndAggregateUsageStats(anyLong(), anyLong())) + .thenReturn(mapOf(GMAIL_PACKAGE_NAME to UsageStats().apply { mAppLaunchCount = 2 })) + `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) + + val result = educationFilter.shouldShowAppHandleEducation(GMAIL_PACKAGE_NAME) + + // Result should be false as queried usage stats should be considered to determine the result + // instead of cached stats + assertThat(result).isFalse() + } + + companion object { + private const val GMAIL_PACKAGE_NAME = "com.google.android.gm" + private const val YOUTUBE_PACKAGE_NAME = "com.google.android.youtube" + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt new file mode 100644 index 000000000000..9b9703fdf6dc --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2024 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.desktopmode.persistence + +import android.content.Context +import android.platform.test.annotations.EnableFlags +import android.testing.AndroidTestingRunner +import android.util.ArraySet +import android.view.Display.DEFAULT_DISPLAY +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import androidx.test.core.app.ApplicationProvider +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE +import com.android.wm.shell.ShellTestCase +import com.google.common.truth.Truth.assertThat +import java.io.File +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@ExperimentalCoroutinesApi +@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE, FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) +class DesktopPersistentRepositoryTest : ShellTestCase() { + private val testContext: Context = InstrumentationRegistry.getInstrumentation().targetContext + private lateinit var testDatastore: DataStore<DesktopPersistentRepositories> + private lateinit var datastoreRepository: DesktopPersistentRepository + private lateinit var datastoreScope: CoroutineScope + + @Before + fun setUp() { + Dispatchers.setMain(StandardTestDispatcher()) + datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + testDatastore = + DataStoreFactory.create( + serializer = + DesktopPersistentRepository.Companion.DesktopPersistentRepositoriesSerializer, + scope = datastoreScope) { + testContext.dataStoreFile(DESKTOP_REPOSITORY_STATES_DATASTORE_TEST_FILE) + } + datastoreRepository = DesktopPersistentRepository(testDatastore) + } + + @After + fun tearDown() { + File(ApplicationProvider.getApplicationContext<Context>().filesDir, "datastore") + .deleteRecursively() + + datastoreScope.cancel() + } + + @Test + fun readRepository_returnsCorrectDesktop() { + runTest(StandardTestDispatcher()) { + val task = createDesktopTask(1) + val desk = createDesktop(task) + val repositoryState = + DesktopRepositoryState.newBuilder().putDesktop(DEFAULT_DESKTOP_ID, desk) + val DesktopPersistentRepositories = + DesktopPersistentRepositories.newBuilder() + .putDesktopRepoByUser(DEFAULT_USER_ID, repositoryState.build()) + .build() + testDatastore.updateData { DesktopPersistentRepositories } + + val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID) + + assertThat(actualDesktop).isEqualTo(desk) + } + } + + @Test + fun addOrUpdateTask_addNewTaskToDesktop() { + runTest(StandardTestDispatcher()) { + // Create a basic repository state + val task = createDesktopTask(1) + val DesktopPersistentRepositories = createRepositoryWithOneDesk(task) + testDatastore.updateData { DesktopPersistentRepositories } + // Create a new state to be initialized + val visibleTasks = ArraySet(listOf(1, 2)) + val minimizedTasks = ArraySet<Int>() + val freeformTasksInZOrder = ArrayList(listOf(2, 1)) + + // Update with new state + datastoreRepository.addOrUpdateDesktop( + visibleTasks = visibleTasks, + minimizedTasks = minimizedTasks, + freeformTasksInZOrder = freeformTasksInZOrder) + + val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID) + assertThat(actualDesktop.tasksByTaskIdMap).hasSize(2) + assertThat(actualDesktop.getZOrderedTasks(0)).isEqualTo(2) + } + } + + @Test + fun addOrUpdateTask_changeTaskStateToMinimize_taskStateIsMinimized() { + runTest(StandardTestDispatcher()) { + val task = createDesktopTask(1) + val DesktopPersistentRepositories = createRepositoryWithOneDesk(task) + testDatastore.updateData { DesktopPersistentRepositories } + // Create a new state to be initialized + val visibleTasks = ArraySet(listOf(1)) + val minimizedTasks = ArraySet(listOf(1)) + val freeformTasksInZOrder = ArrayList(listOf(1)) + + // Update with new state + datastoreRepository.addOrUpdateDesktop( + visibleTasks = visibleTasks, + minimizedTasks = minimizedTasks, + freeformTasksInZOrder = freeformTasksInZOrder) + + val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID) + assertThat(actualDesktop.tasksByTaskIdMap[task.taskId]?.desktopTaskState) + .isEqualTo(DesktopTaskState.MINIMIZED) + } + } + + @Test + fun removeTask_previouslyAddedTaskIsRemoved() { + runTest(StandardTestDispatcher()) { + val task = createDesktopTask(1) + val DesktopPersistentRepositories = createRepositoryWithOneDesk(task) + testDatastore.updateData { DesktopPersistentRepositories } + // Create a new state to be initialized + val visibleTasks = ArraySet<Int>() + val minimizedTasks = ArraySet<Int>() + val freeformTasksInZOrder = ArrayList<Int>() + + // Update with new state + datastoreRepository.addOrUpdateDesktop( + visibleTasks = visibleTasks, + minimizedTasks = minimizedTasks, + freeformTasksInZOrder = freeformTasksInZOrder) + + val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID) + assertThat(actualDesktop.tasksByTaskIdMap).isEmpty() + assertThat(actualDesktop.zOrderedTasksList).isEmpty() + } + } + + private companion object { + const val DESKTOP_REPOSITORY_STATES_DATASTORE_TEST_FILE = "desktop_repo_test.pb" + const val DEFAULT_USER_ID = 1000 + const val DEFAULT_DESKTOP_ID = 0 + + fun createRepositoryWithOneDesk(task: DesktopTask): DesktopPersistentRepositories { + val desk = createDesktop(task) + val repositoryState = + DesktopRepositoryState.newBuilder().putDesktop(DEFAULT_DESKTOP_ID, desk) + val DesktopPersistentRepositories = + DesktopPersistentRepositories.newBuilder() + .putDesktopRepoByUser(DEFAULT_USER_ID, repositoryState.build()) + .build() + return DesktopPersistentRepositories + } + + fun createDesktop(task: DesktopTask): Desktop? = + Desktop.newBuilder() + .setDisplayId(DEFAULT_DISPLAY) + .addZOrderedTasks(task.taskId) + .putTasksByTaskId(task.taskId, task) + .build() + + fun createDesktopTask( + taskId: Int, + state: DesktopTaskState = DesktopTaskState.VISIBLE + ): DesktopTask = + DesktopTask.newBuilder().setTaskId(taskId).setDesktopTaskState(state).build() + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java index a64ebd301c00..840126421c08 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java @@ -76,6 +76,8 @@ public class DragAndDropControllerTest extends ShellTestCase { @Mock private ShellCommandHandler mShellCommandHandler; @Mock + private ShellTaskOrganizer mShellTaskOrganizer; + @Mock private DisplayController mDisplayController; @Mock private UiEventLogger mUiEventLogger; @@ -96,8 +98,8 @@ public class DragAndDropControllerTest extends ShellTestCase { public void setUp() throws RemoteException { MockitoAnnotations.initMocks(this); mController = new DragAndDropController(mContext, mShellInit, mShellController, - mShellCommandHandler, mDisplayController, mUiEventLogger, mIconProvider, - mGlobalDragListener, mTransitions, mMainExecutor); + mShellCommandHandler, mShellTaskOrganizer, mDisplayController, mUiEventLogger, + mIconProvider, mGlobalDragListener, mTransitions, mMainExecutor); mController.onInit(); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/OWNERS new file mode 100644 index 000000000000..cb124016ca6f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 1214056 +# includes OWNERS from parent directories
\ No newline at end of file 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/SplitDragPolicyTest.java index 6e72e8df8d62..46b60499a01d 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/SplitDragPolicyTest.java @@ -27,14 +27,14 @@ import static android.content.ClipDescription.MIMETYPE_TEXT_INTENT; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitScreenConstants.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; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT; -import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_FULLSCREEN; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_BOTTOM; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_LEFT; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_RIGHT; +import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_TOP; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; @@ -65,8 +65,6 @@ import android.content.res.Resources; import android.graphics.Insets; import android.os.RemoteException; import android.view.DisplayInfo; -import android.view.DragEvent; -import android.view.View; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -74,9 +72,8 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.InstanceId; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.draganddrop.DragAndDropPolicy.Target; +import com.android.wm.shell.draganddrop.SplitDragPolicy.Target; import com.android.wm.shell.splitscreen.SplitScreenController; -import com.android.wm.shell.startingsurface.TaskSnapshotWindow; import org.junit.After; import org.junit.Before; @@ -95,7 +92,7 @@ import java.util.HashSet; */ @SmallTest @RunWith(AndroidJUnit4.class) -public class DragAndDropPolicyTest extends ShellTestCase { +public class SplitDragPolicyTest extends ShellTestCase { @Mock private Context mContext; @@ -106,6 +103,8 @@ public class DragAndDropPolicyTest extends ShellTestCase { // Both the split-screen and start interface. @Mock private SplitScreenController mSplitScreenStarter; + @Mock + private SplitDragPolicy.Starter mFullscreenStarter; @Mock private InstanceId mLoggerSessionId; @@ -113,7 +112,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { private DisplayLayout mLandscapeDisplayLayout; private DisplayLayout mPortraitDisplayLayout; private Insets mInsets; - private DragAndDropPolicy mPolicy; + private SplitDragPolicy mPolicy; private ClipData mActivityClipData; private PendingIntent mLaunchableIntentPendingIntent; @@ -151,7 +150,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { mPortraitDisplayLayout = new DisplayLayout(info2, res, false, false); mInsets = Insets.of(0, 0, 0, 0); - mPolicy = spy(new DragAndDropPolicy(mContext, mSplitScreenStarter, mSplitScreenStarter)); + mPolicy = spy(new SplitDragPolicy(mContext, mSplitScreenStarter, mFullscreenStarter)); mActivityClipData = createAppClipData(MIMETYPE_APPLICATION_ACTIVITY); mLaunchableIntentPendingIntent = mock(PendingIntent.class); when(mLaunchableIntentPendingIntent.getCreatorUserHandle()) @@ -285,14 +284,14 @@ public class DragAndDropPolicyTest extends ShellTestCase { setRunningTask(mHomeTask); DragSession dragSession = new DragSession(mActivityTaskManager, mLandscapeDisplayLayout, data, 0 /* dragFlags */); - dragSession.update(); + dragSession.initialize(); mPolicy.start(dragSession, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( mPolicy.getTargets(mInsets), TYPE_FULLSCREEN); - mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN)); - verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), - eq(SPLIT_POSITION_UNDEFINED), any()); + mPolicy.onDropped(filterTargetByType(targets, TYPE_FULLSCREEN), null /* hideTaskToken */); + verify(mFullscreenStarter).startIntent(any(), anyInt(), any(), + eq(SPLIT_POSITION_UNDEFINED), any(), any()); } private void dragOverFullscreenApp_expectSplitScreenTargets(ClipData data) { @@ -300,19 +299,19 @@ public class DragAndDropPolicyTest extends ShellTestCase { setRunningTask(mFullscreenAppTask); DragSession dragSession = new DragSession(mActivityTaskManager, mLandscapeDisplayLayout, data, 0 /* dragFlags */); - dragSession.update(); + dragSession.initialize(); mPolicy.start(dragSession, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); - mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_LEFT)); + mPolicy.onDropped(filterTargetByType(targets, TYPE_SPLIT_LEFT), null /* hideTaskToken */); verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), - eq(SPLIT_POSITION_TOP_OR_LEFT), any()); + eq(SPLIT_POSITION_TOP_OR_LEFT), any(), any()); reset(mSplitScreenStarter); - mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT)); + mPolicy.onDropped(filterTargetByType(targets, TYPE_SPLIT_RIGHT), null /* hideTaskToken */); verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), - eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()); + eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any(), any()); } private void dragOverFullscreenAppPhone_expectVerticalSplitScreenTargets(ClipData data) { @@ -320,19 +319,20 @@ public class DragAndDropPolicyTest extends ShellTestCase { setRunningTask(mFullscreenAppTask); DragSession dragSession = new DragSession(mActivityTaskManager, mPortraitDisplayLayout, data, 0 /* dragFlags */); - dragSession.update(); + dragSession.initialize(); mPolicy.start(dragSession, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM); - mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_TOP)); + mPolicy.onDropped(filterTargetByType(targets, TYPE_SPLIT_TOP), null /* hideTaskToken */); verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), - eq(SPLIT_POSITION_TOP_OR_LEFT), any()); + eq(SPLIT_POSITION_TOP_OR_LEFT), any(), any()); reset(mSplitScreenStarter); - mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM)); + mPolicy.onDropped(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), + null /* hideTaskToken */); verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), - eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()); + eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any(), any()); } @Test @@ -340,7 +340,7 @@ public class DragAndDropPolicyTest extends ShellTestCase { setRunningTask(mFullscreenAppTask); DragSession dragSession = new DragSession(mActivityTaskManager, mLandscapeDisplayLayout, mActivityClipData, 0 /* dragFlags */); - dragSession.update(); + dragSession.initialize(); mPolicy.start(dragSession, mLoggerSessionId); ArrayList<Target> targets = mPolicy.getTargets(mInsets); for (Target t : targets) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java index 3f3cafcf6375..763d0153071e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java @@ -27,6 +27,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.app.ActivityManager; +import android.view.SurfaceControl; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -35,8 +36,9 @@ import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; +import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; -import com.android.wm.shell.shared.DesktopModeStatus; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.windowdecor.WindowDecorViewModel; @@ -65,7 +67,11 @@ public final class FreeformTaskListenerTests extends ShellTestCase { @Mock private WindowDecorViewModel mWindowDecorViewModel; @Mock + private SurfaceControl mMockSurfaceControl; + @Mock private DesktopModeTaskRepository mDesktopModeTaskRepository; + @Mock + private LaunchAdjacentController mLaunchAdjacentController; private FreeformTaskListener mFreeformTaskListener; private StaticMockitoSession mMockitoSession; @@ -80,6 +86,7 @@ public final class FreeformTaskListenerTests extends ShellTestCase { mShellInit, mTaskOrganizer, Optional.of(mDesktopModeTaskRepository), + mLaunchAdjacentController, mWindowDecorViewModel); } @@ -107,6 +114,31 @@ public final class FreeformTaskListenerTests extends ShellTestCase { .addOrMoveFreeformTaskToTop(fullscreenTask.displayId, fullscreenTask.taskId); } + @Test + public void testVisibilityTaskChanged_visible_setLaunchAdjacentDisabled() { + ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder() + .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + task.isVisible = true; + + mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl); + + verify(mLaunchAdjacentController).setLaunchAdjacentEnabled(false); + } + + @Test + public void testVisibilityTaskChanged_NotVisible_setLaunchAdjacentEnabled() { + ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder() + .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + task.isVisible = true; + + mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl); + + task.isVisible = false; + mFreeformTaskListener.onTaskInfoChanged(task); + + verify(mLaunchAdjacentController).setLaunchAdjacentEnabled(true); + } + @After public void tearDown() { mMockitoSession.finishMocking(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/OWNERS new file mode 100644 index 000000000000..553540cbb86c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 929241 +# includes OWNERS from parent directories
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OWNERS new file mode 100644 index 000000000000..b66cfc336f7f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 785166 +# includes OWNERS from parent directories
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java index 8ad3d2a72617..7d063a0a773f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java @@ -48,10 +48,10 @@ 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.TaskStackListenerImpl; +import com.android.wm.shell.shared.ShellSharedConstants; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; -import com.android.wm.shell.sysui.ShellSharedConstants; import org.junit.Before; import org.junit.Test; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java index 9c7f7237871a..9146906b6385 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java @@ -37,6 +37,7 @@ import static org.mockito.Mockito.when; import android.content.res.Configuration; import android.graphics.Rect; import android.os.Binder; +import android.os.Handler; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.Display; @@ -100,6 +101,8 @@ public class OneHandedDisplayAreaOrganizerTest extends OneHandedTestCase { OneHandedSettingsUtil mMockSettingsUitl; @Mock InteractionJankMonitor mJankMonitor; + @Mock + Handler mMockHandler; List<DisplayAreaAppearedInfo> mDisplayAreaAppearedInfoList = new ArrayList<>(); @@ -142,7 +145,8 @@ public class OneHandedDisplayAreaOrganizerTest extends OneHandedTestCase { mMockAnimationController, mTutorialHandler, mJankMonitor, - mMockShellMainExecutor)); + mMockShellMainExecutor, + mMockHandler)); for (int i = 0; i < DISPLAYAREA_INFO_COUNT; i++) { mDisplayAreaAppearedInfoList.add(getDummyDisplayAreaInfo()); @@ -429,7 +433,8 @@ public class OneHandedDisplayAreaOrganizerTest extends OneHandedTestCase { mMockAnimationController, mTutorialHandler, mJankMonitor, - mMockShellMainExecutor)); + mMockShellMainExecutor, + mMockHandler)); assertThat(testSpiedDisplayAreaOrganizer.isReady()).isFalse(); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/OWNERS new file mode 100644 index 000000000000..ad3ca733db12 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 316251 +# includes OWNERS from parent directories
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java index 5880ffb0dce2..72950a8dc139 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java @@ -88,8 +88,11 @@ public class PipAnimationControllerTest extends ShellTestCase { @Test public void getAnimator_withBounds_returnBoundsAnimator() { + final Rect baseValue = new Rect(0, 0, 100, 100); + final Rect startValue = new Rect(0, 0, 100, 100); + final Rect endValue1 = new Rect(100, 100, 200, 200); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController - .getAnimator(mTaskInfo, mLeash, new Rect(), new Rect(), new Rect(), null, + .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null, TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0); assertEquals("Expect ANIM_TYPE_BOUNDS animation", diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java index 8c7b47ea7d84..e3798e92c092 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java @@ -109,6 +109,7 @@ public class PhonePipKeepClearAlgorithmTest extends ShellTestCase { final Rect pipBounds = new Rect(0, 0, 100, 100); final Rect keepClearRect = new Rect(50, 50, 150, 150); when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds); + when(mMockPipBoundsState.getRestoreBounds()).thenReturn(new Rect(0, 0, 0, 0)); when(mMockPipBoundsState.getRestrictedKeepClearAreas()).thenReturn(Set.of(keepClearRect)); doAnswer(invocation -> { Rect arg0 = invocation.getArgument(0); @@ -127,6 +128,7 @@ public class PhonePipKeepClearAlgorithmTest extends ShellTestCase { final Rect pipBounds = new Rect(0, 0, 100, 100); final Rect keepClearRect = new Rect(100, 100, 150, 150); when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds); + when(mMockPipBoundsState.getRestoreBounds()).thenReturn(new Rect(0, 0, 0, 0)); when(mMockPipBoundsState.getRestrictedKeepClearAreas()).thenReturn(Set.of(keepClearRect)); doAnswer(invocation -> { Rect arg0 = invocation.getArgument(0); @@ -145,6 +147,7 @@ public class PhonePipKeepClearAlgorithmTest extends ShellTestCase { final Rect pipBounds = new Rect(0, 0, 100, 100); final Rect keepClearRect = new Rect(50, 50, 150, 150); when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds); + when(mMockPipBoundsState.getRestoreBounds()).thenReturn(new Rect(0, 0, 0, 0)); when(mMockPipBoundsState.isStashed()).thenReturn(true); when(mMockPipBoundsState.getRestrictedKeepClearAreas()).thenReturn(Set.of(keepClearRect)); doAnswer(invocation -> { @@ -164,6 +167,7 @@ public class PhonePipKeepClearAlgorithmTest extends ShellTestCase { final Rect pipBounds = new Rect(0, 0, 100, 100); final Rect keepClearRect = new Rect(100, 100, 150, 150); when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds); + when(mMockPipBoundsState.getRestoreBounds()).thenReturn(new Rect(0, 0, 0, 0)); when(mMockPipBoundsState.isStashed()).thenReturn(true); when(mMockPipBoundsState.getRestrictedKeepClearAreas()).thenReturn(Set.of(keepClearRect)); doAnswer(invocation -> { @@ -185,6 +189,7 @@ public class PhonePipKeepClearAlgorithmTest extends ShellTestCase { final Rect expected = new Rect( 0, DISPLAY_BOUNDS.bottom - 100, 100, DISPLAY_BOUNDS.bottom); when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds); + when(mMockPipBoundsState.getRestoreBounds()).thenReturn(new Rect(0, 0, 0, 0)); doAnswer(invocation -> { Rect arg0 = invocation.getArgument(0); arg0.set(DISPLAY_BOUNDS); @@ -205,6 +210,7 @@ public class PhonePipKeepClearAlgorithmTest extends ShellTestCase { final Rect expected = new Rect( 0, DISPLAY_BOUNDS.bottom - 100, 100, DISPLAY_BOUNDS.bottom); when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds); + when(mMockPipBoundsState.getRestoreBounds()).thenReturn(new Rect(0, 0, 0, 0)); doAnswer(invocation -> { Rect arg0 = invocation.getArgument(0); arg0.set(DISPLAY_BOUNDS); @@ -227,6 +233,7 @@ public class PhonePipKeepClearAlgorithmTest extends ShellTestCase { DISPLAY_BOUNDS.right - 100, DISPLAY_BOUNDS.bottom - 100, DISPLAY_BOUNDS.right, DISPLAY_BOUNDS.bottom); when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds); + when(mMockPipBoundsState.getRestoreBounds()).thenReturn(new Rect(0, 0, 0, 0)); doAnswer(invocation -> { Rect arg0 = invocation.getArgument(0); arg0.set(DISPLAY_BOUNDS); @@ -249,6 +256,7 @@ public class PhonePipKeepClearAlgorithmTest extends ShellTestCase { DISPLAY_BOUNDS.right - 100, DISPLAY_BOUNDS.bottom - 100, DISPLAY_BOUNDS.right, DISPLAY_BOUNDS.bottom); when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds); + when(mMockPipBoundsState.getRestoreBounds()).thenReturn(new Rect(0, 0, 0, 0)); doAnswer(invocation -> { Rect arg0 = invocation.getArgument(0); arg0.set(DISPLAY_BOUNDS); @@ -269,6 +277,7 @@ public class PhonePipKeepClearAlgorithmTest extends ShellTestCase { final Rect expected = new Rect( 0, DISPLAY_BOUNDS.bottom - 100, 100, DISPLAY_BOUNDS.bottom); when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds); + when(mMockPipBoundsState.getRestoreBounds()).thenReturn(new Rect(0, 0, 0, 0)); when(mMockPipBoundsState.isStashed()).thenReturn(true); doAnswer(invocation -> { Rect arg0 = invocation.getArgument(0); @@ -289,6 +298,7 @@ public class PhonePipKeepClearAlgorithmTest extends ShellTestCase { final Rect expected = new Rect( 0, DISPLAY_BOUNDS.bottom - 100, 100, DISPLAY_BOUNDS.bottom); when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds); + when(mMockPipBoundsState.getRestoreBounds()).thenReturn(new Rect(0, 0, 0, 0)); when(mMockPipBoundsState.isStashed()).thenReturn(true); doAnswer(invocation -> { Rect arg0 = invocation.getArgument(0); @@ -301,4 +311,40 @@ public class PhonePipKeepClearAlgorithmTest extends ShellTestCase { assertEquals(expected, outBounds); } + + @Test + public void adjust_restoreBoundsPresent_appliesRestoreBounds() { + final Rect pipBounds = new Rect(0, 0, 100, 100); + final Rect restoreBounds = new Rect(50, 50, 150, 150); + when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds); + when(mMockPipBoundsState.getRestoreBounds()).thenReturn(restoreBounds); + when(mMockPipBoundsState.hasUserMovedPip()).thenReturn(true); + doAnswer(invocation -> { + Rect arg0 = invocation.getArgument(0); + arg0.set(DISPLAY_BOUNDS); + return null; + }).when(mMockPipBoundsAlgorithm).getInsetBounds(any(Rect.class)); + + final Rect outBounds = mPipKeepClearAlgorithm.adjust( + mMockPipBoundsState, mMockPipBoundsAlgorithm); + assertEquals(restoreBounds, outBounds); + } + + @Test + public void adjust_restoreBoundsCleared_boundsUnchanged() { + final Rect pipBounds = new Rect(0, 0, 100, 100); + final Rect restoreBounds = new Rect(0, 0, 0, 0); + when(mMockPipBoundsState.getBounds()).thenReturn(pipBounds); + when(mMockPipBoundsState.getRestoreBounds()).thenReturn(restoreBounds); + when(mMockPipBoundsState.hasUserMovedPip()).thenReturn(true); + doAnswer(invocation -> { + Rect arg0 = invocation.getArgument(0); + arg0.set(DISPLAY_BOUNDS); + return null; + }).when(mMockPipBoundsAlgorithm).getInsetBounds(any(Rect.class)); + + final Rect outBounds = mPipKeepClearAlgorithm.adjust( + mMockPipBoundsState, mMockPipBoundsAlgorithm); + assertEquals(pipBounds, outBounds); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java index d38fc6cb6418..96003515a485 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -34,13 +34,13 @@ import static org.mockito.Mockito.when; import static java.lang.Integer.MAX_VALUE; -import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; import android.os.Bundle; +import android.os.Handler; import android.os.RemoteException; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -68,10 +68,10 @@ import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipTransitionState; +import com.android.wm.shell.shared.ShellSharedConstants; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; -import com.android.wm.shell.sysui.ShellSharedConstants; import org.junit.Before; import org.junit.Test; @@ -116,6 +116,7 @@ public class PipControllerTest extends ShellTestCase { @Mock private PipParamsChangedForwarder mMockPipParamsChangedForwarder; @Mock private DisplayInsetsController mMockDisplayInsetsController; @Mock private TabletopModeController mMockTabletopModeController; + @Mock private Handler mMockHandler; @Mock private DisplayLayout mMockDisplayLayout1; @Mock private DisplayLayout mMockDisplayLayout2; @@ -139,7 +140,7 @@ public class PipControllerTest extends ShellTestCase { mMockPipTransitionController, mMockWindowManagerShellWrapper, mMockTaskStackListener, mMockPipParamsChangedForwarder, mMockDisplayInsetsController, mMockTabletopModeController, - mMockOneHandedController, mMockExecutor); + mMockOneHandedController, mMockExecutor, mMockHandler); mShellInit.init(); when(mMockPipBoundsAlgorithm.getSnapAlgorithm()).thenReturn(mMockPipSnapAlgorithm); when(mMockPipTouchHandler.getMotionHelper()).thenReturn(mMockPipMotionHelper); @@ -183,7 +184,7 @@ public class PipControllerTest extends ShellTestCase { @Test public void instantiatePipController_registersPipTransitionCallback() { - verify(mMockPipTransitionController).registerPipTransitionCallback(any()); + verify(mMockPipTransitionController).registerPipTransitionCallback(any(), any()); } @Test @@ -231,28 +232,7 @@ public class PipControllerTest extends ShellTestCase { mMockPipTransitionController, mMockWindowManagerShellWrapper, mMockTaskStackListener, mMockPipParamsChangedForwarder, mMockDisplayInsetsController, mMockTabletopModeController, - mMockOneHandedController, mMockExecutor)); - } - - @Test - public void onActivityHidden_isLastPipComponentName_clearLastPipComponent() { - final ComponentName component1 = new ComponentName(mContext, "component1"); - when(mMockPipBoundsState.getLastPipComponentName()).thenReturn(component1); - - mPipController.mPinnedTaskListener.onActivityHidden(component1); - - verify(mMockPipBoundsState).setLastPipComponentName(null); - } - - @Test - public void onActivityHidden_isNotLastPipComponentName_lastPipComponentNotCleared() { - final ComponentName component1 = new ComponentName(mContext, "component1"); - final ComponentName component2 = new ComponentName(mContext, "component2"); - when(mMockPipBoundsState.getLastPipComponentName()).thenReturn(component1); - - mPipController.mPinnedTaskListener.onActivityHidden(component2); - - verify(mMockPipBoundsState, never()).setLastPipComponentName(null); + mMockOneHandedController, mMockExecutor, mMockHandler)); } @Test @@ -278,7 +258,7 @@ public class PipControllerTest extends ShellTestCase { when(mMockPipDisplayLayoutState.getDisplayLayout()).thenReturn(mMockDisplayLayout1); when(mMockDisplayController.getDisplayLayout(displayId)).thenReturn(mMockDisplayLayout2); - when(mMockPipTaskOrganizer.isInPip()).thenReturn(true); + when(mMockPipTransitionState.hasEnteredPip()).thenReturn(true); mPipController.mDisplaysChangedListener.onDisplayConfigurationChanged( displayId, new Configuration()); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java index ace09a82d71c..66f8c0b9558d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java @@ -114,8 +114,8 @@ public class PipResizeGestureHandlerTest extends ShellTestCase { final PipBoundsAlgorithm pipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, pipSnapAlgorithm, pipKeepClearAlgorithm, mPipDisplayLayoutState, mSizeSpecSource); - final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mPipBoundsState, - mPipTaskOrganizer, mPhonePipMenuController, pipSnapAlgorithm, + final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mMainExecutor, + mPipBoundsState, mPipTaskOrganizer, mPhonePipMenuController, pipSnapAlgorithm, mMockPipTransitionController, mFloatingContentCoordinator, Optional.empty() /* pipPerfHintControllerOptional */); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java index 92762fa68550..6d18e3696f84 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java @@ -116,8 +116,8 @@ public class PipTouchHandlerTest extends ShellTestCase { mPipSnapAlgorithm = new PipSnapAlgorithm(); mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm, new PipKeepClearAlgorithmInterface() {}, mPipDisplayLayoutState, mSizeSpecSource); - PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mPipBoundsState, - mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm, + PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mMainExecutor, + mPipBoundsState, mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm, mMockPipTransitionController, mFloatingContentCoordinator, Optional.empty() /* pipPerfHintControllerOptional */); mPipTouchHandler = new PipTouchHandler(mContext, mShellInit, mPhonePipMenuController, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java index 974539f23b80..aa2d6f09508f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java @@ -241,16 +241,16 @@ public class TvPipGravityTest extends ShellTestCase { @Test public void updateGravity_move_expanded_valid() { - mTvPipBoundsState.setTvPipExpanded(true); - // Vertical expanded PiP. mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true); + mTvPipBoundsState.setTvPipExpanded(true); mTvPipBoundsState.setTvPipGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT); moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.CENTER_VERTICAL | Gravity.LEFT, true); moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.CENTER_VERTICAL | Gravity.RIGHT, true); // Horizontal expanded PiP. mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true); + mTvPipBoundsState.setTvPipExpanded(true); mTvPipBoundsState.setTvPipGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.TOP | Gravity.CENTER_HORIZONTAL, true); moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, true); @@ -281,10 +281,9 @@ public class TvPipGravityTest extends ShellTestCase { @Test public void updateGravity_move_expanded_invalid() { - mTvPipBoundsState.setTvPipExpanded(true); - // Vertical expanded PiP. mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true); + mTvPipBoundsState.setTvPipExpanded(true); mTvPipBoundsState.setTvPipGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT); moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.CENTER_VERTICAL | Gravity.RIGHT, false); moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.CENTER_VERTICAL | Gravity.RIGHT, false); @@ -297,6 +296,7 @@ public class TvPipGravityTest extends ShellTestCase { // Horizontal expanded PiP. mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true); + mTvPipBoundsState.setTvPipExpanded(true); mTvPipBoundsState.setTvPipGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, false); moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, false); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/OWNERS new file mode 100644 index 000000000000..ad3ca733db12 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 316251 +# includes OWNERS from parent directories
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTransitionStateTest.java index f3f3c37b645d..571ae93e1aec 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTransitionStateTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip2; +package com.android.wm.shell.pip2.phone; import android.os.Bundle; import android.os.Handler; @@ -22,8 +22,6 @@ import android.os.Parcelable; import android.testing.AndroidTestingRunner; import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.common.pip.PhoneSizeSpecSource; -import com.android.wm.shell.pip2.phone.PipTransitionState; import junit.framework.Assert; @@ -33,7 +31,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; /** - * Unit test against {@link PhoneSizeSpecSource}. + * Unit test against {@link PipTransitionState}. * * This test mocks the PiP2 flag to be true. */ diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipUiStateChangeControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipUiStateChangeControllerTests.java new file mode 100644 index 000000000000..82cdfd52d2db --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipUiStateChangeControllerTests.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2024 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.pip2.phone; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.app.Flags; +import android.app.PictureInPictureUiState; +import android.os.Bundle; +import android.platform.test.annotations.EnableFlags; +import android.testing.AndroidTestingRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.function.Consumer; + +/** + * Unit test against {@link PipUiStateChangeController}. + */ +@RunWith(AndroidTestingRunner.class) +public class PipUiStateChangeControllerTests { + + @Mock + private PipTransitionState mPipTransitionState; + + private Consumer<PictureInPictureUiState> mPictureInPictureUiStateConsumer; + private ArgumentCaptor<PictureInPictureUiState> mArgumentCaptor; + + private PipUiStateChangeController mPipUiStateChangeController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mPipUiStateChangeController = new PipUiStateChangeController(mPipTransitionState); + mPictureInPictureUiStateConsumer = spy(pictureInPictureUiState -> {}); + mPipUiStateChangeController.setPictureInPictureUiStateConsumer( + mPictureInPictureUiStateConsumer); + mArgumentCaptor = ArgumentCaptor.forClass(PictureInPictureUiState.class); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_PIP_UI_STATE_CALLBACK_ON_ENTERING) + public void onPipTransitionStateChanged_swipePipStart_callbackIsTransitioningToPipTrue() { + when(mPipTransitionState.isInSwipePipToHomeTransition()).thenReturn(true); + + mPipUiStateChangeController.onPipTransitionStateChanged( + PipTransitionState.UNDEFINED, PipTransitionState.SWIPING_TO_PIP, Bundle.EMPTY); + + verify(mPictureInPictureUiStateConsumer).accept(mArgumentCaptor.capture()); + assertTrue(mArgumentCaptor.getValue().isTransitioningToPip()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_PIP_UI_STATE_CALLBACK_ON_ENTERING) + public void onPipTransitionStateChanged_swipePipOngoing_noCallbackIsTransitioningToPip() { + when(mPipTransitionState.isInSwipePipToHomeTransition()).thenReturn(true); + + mPipUiStateChangeController.onPipTransitionStateChanged( + PipTransitionState.SWIPING_TO_PIP, PipTransitionState.ENTERING_PIP, Bundle.EMPTY); + + verifyZeroInteractions(mPictureInPictureUiStateConsumer); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_PIP_UI_STATE_CALLBACK_ON_ENTERING) + public void onPipTransitionStateChanged_swipePipFinish_callbackIsTransitioningToPipFalse() { + when(mPipTransitionState.isInSwipePipToHomeTransition()).thenReturn(true); + + mPipUiStateChangeController.onPipTransitionStateChanged( + PipTransitionState.SWIPING_TO_PIP, PipTransitionState.ENTERED_PIP, Bundle.EMPTY); + + verify(mPictureInPictureUiStateConsumer).accept(mArgumentCaptor.capture()); + assertFalse(mArgumentCaptor.getValue().isTransitioningToPip()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_PIP_UI_STATE_CALLBACK_ON_ENTERING) + public void onPipTransitionStateChanged_tapHomeStart_callbackIsTransitioningToPipTrue() { + when(mPipTransitionState.isInSwipePipToHomeTransition()).thenReturn(false); + + mPipUiStateChangeController.onPipTransitionStateChanged( + PipTransitionState.UNDEFINED, PipTransitionState.ENTERING_PIP, Bundle.EMPTY); + + verify(mPictureInPictureUiStateConsumer).accept(mArgumentCaptor.capture()); + assertTrue(mArgumentCaptor.getValue().isTransitioningToPip()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_PIP_UI_STATE_CALLBACK_ON_ENTERING) + public void onPipTransitionStateChanged_tapHomeFinish_callbackIsTransitioningToPipFalse() { + when(mPipTransitionState.isInSwipePipToHomeTransition()).thenReturn(false); + + mPipUiStateChangeController.onPipTransitionStateChanged( + PipTransitionState.ENTERING_PIP, PipTransitionState.ENTERED_PIP, Bundle.EMPTY); + + verify(mPictureInPictureUiStateConsumer).accept(mArgumentCaptor.capture()); + assertFalse(mArgumentCaptor.getValue().isTransitioningToPip()); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt index bbd65be9abda..0c3f98a324cd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt @@ -24,15 +24,16 @@ import android.window.IWindowContainerToken import android.window.WindowContainerToken import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTestCase -import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50 -import com.android.wm.shell.util.GroupedRecentTaskInfo -import com.android.wm.shell.util.GroupedRecentTaskInfo.CREATOR -import com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_FREEFORM -import com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_SINGLE -import com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_SPLIT -import com.android.wm.shell.util.SplitBounds +import com.android.wm.shell.shared.GroupedRecentTaskInfo +import com.android.wm.shell.shared.GroupedRecentTaskInfo.CREATOR +import com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_FREEFORM +import com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_SINGLE +import com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_SPLIT +import com.android.wm.shell.shared.split.SplitBounds +import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50 import com.google.common.truth.Correspondence import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.mock @@ -86,12 +87,13 @@ class GroupedRecentTaskInfoTest : ShellTestCase() { @Test fun testFreeformTasks_hasCorrectType() { - assertThat(freeformTasksGroupInfo().type).isEqualTo(TYPE_FREEFORM) + assertThat(freeformTasksGroupInfo(freeformTaskIds = arrayOf(1)).type) + .isEqualTo(TYPE_FREEFORM) } @Test - fun testSplitTasks_taskInfoList_hasThreeTasks() { - val list = freeformTasksGroupInfo().taskInfoList + fun testCreateFreeformTasks_hasCorrectNumberOfTasks() { + val list = freeformTasksGroupInfo(freeformTaskIds = arrayOf(1, 2, 3)).taskInfoList assertThat(list).hasSize(3) assertThat(list[0].taskId).isEqualTo(1) assertThat(list[1].taskId).isEqualTo(2) @@ -99,6 +101,16 @@ class GroupedRecentTaskInfoTest : ShellTestCase() { } @Test + fun testCreateFreeformTasks_nonExistentMinimizedTaskId_throwsException() { + assertThrows(IllegalArgumentException::class.java) { + freeformTasksGroupInfo( + freeformTaskIds = arrayOf(1, 2, 3), + minimizedTaskIds = arrayOf(1, 4) + ) + } + } + + @Test fun testParcelling_singleTask() { val recentTaskInfo = singleTaskGroupInfo() val parcel = Parcel.obtain() @@ -129,7 +141,7 @@ class GroupedRecentTaskInfoTest : ShellTestCase() { @Test fun testParcelling_freeformTasks() { - val recentTaskInfo = freeformTasksGroupInfo() + val recentTaskInfo = freeformTasksGroupInfo(freeformTaskIds = arrayOf(1, 2, 3)) val parcel = Parcel.obtain() recentTaskInfo.writeToParcel(parcel, 0) parcel.setDataPosition(0) @@ -145,6 +157,21 @@ class GroupedRecentTaskInfoTest : ShellTestCase() { .containsExactly(1, 2, 3) } + @Test + fun testParcelling_freeformTasks_minimizedTasks() { + val recentTaskInfo = freeformTasksGroupInfo( + freeformTaskIds = arrayOf(1, 2, 3), minimizedTaskIds = arrayOf(2)) + + val parcel = Parcel.obtain() + recentTaskInfo.writeToParcel(parcel, 0) + parcel.setDataPosition(0) + + // Read the object back from the parcel + val recentTaskInfoParcel = CREATOR.createFromParcel(parcel) + assertThat(recentTaskInfoParcel.type).isEqualTo(TYPE_FREEFORM) + assertThat(recentTaskInfoParcel.minimizedTaskIds).isEqualTo(arrayOf(2).toIntArray()) + } + private fun createTaskInfo(id: Int) = ActivityManager.RecentTaskInfo().apply { taskId = id token = WindowContainerToken(mock(IWindowContainerToken::class.java)) @@ -162,10 +189,12 @@ class GroupedRecentTaskInfoTest : ShellTestCase() { return GroupedRecentTaskInfo.forSplitTasks(task1, task2, splitBounds) } - private fun freeformTasksGroupInfo(): GroupedRecentTaskInfo { - val task1 = createTaskInfo(id = 1) - val task2 = createTaskInfo(id = 2) - val task3 = createTaskInfo(id = 3) - return GroupedRecentTaskInfo.forFreeformTasks(task1, task2, task3) + private fun freeformTasksGroupInfo( + freeformTaskIds: Array<Int>, + minimizedTaskIds: Array<Int> = emptyArray() + ): GroupedRecentTaskInfo { + return GroupedRecentTaskInfo.forFreeformTasks( + freeformTaskIds.map { createTaskInfo(it) }.toTypedArray(), + minimizedTaskIds.toSet()) } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/OWNERS new file mode 100644 index 000000000000..aa019ccd1a31 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 1199235 +# includes OWNERS from parent directories
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java index e291c0e1a151..386253c19c82 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java @@ -22,7 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -46,6 +46,7 @@ import static java.lang.Integer.MAX_VALUE; import android.app.ActivityManager; import android.app.ActivityTaskManager; +import android.app.KeyguardManager; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; @@ -68,13 +69,13 @@ import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; -import com.android.wm.shell.shared.DesktopModeStatus; +import com.android.wm.shell.shared.GroupedRecentTaskInfo; +import com.android.wm.shell.shared.ShellSharedConstants; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.shared.split.SplitBounds; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; -import com.android.wm.shell.sysui.ShellSharedConstants; -import com.android.wm.shell.util.GroupedRecentTaskInfo; -import com.android.wm.shell.util.SplitBounds; import org.junit.After; import org.junit.Before; @@ -136,6 +137,8 @@ public class RecentTasksControllerTest extends ShellTestCase { mMainExecutor = new TestShellExecutor(); when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class)); + when(mContext.getSystemService(KeyguardManager.class)) + .thenReturn(mock(KeyguardManager.class)); mShellInit = spy(new ShellInit(mMainExecutor)); mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler, mDisplayInsetsController, mMainExecutor)); @@ -399,7 +402,7 @@ public class RecentTasksControllerTest extends ShellTestCase { } @Test - public void testGetRecentTasks_proto2Enabled_ignoresMinimizedFreeformTasks() { + public void testGetRecentTasks_proto2Enabled_includesMinimizedFreeformTasks() { ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3); @@ -415,8 +418,7 @@ public class RecentTasksControllerTest extends ShellTestCase { ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks( MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0); - // 2 freeform tasks should be grouped into one, 1 task should be skipped, 3 total recents - // entries + // 3 freeform tasks should be grouped into one, 2 single tasks, 3 total recents entries assertEquals(3, recentTasks.size()); GroupedRecentTaskInfo freeformGroup = recentTasks.get(0); GroupedRecentTaskInfo singleGroup1 = recentTasks.get(1); @@ -428,9 +430,10 @@ public class RecentTasksControllerTest extends ShellTestCase { assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, singleGroup2.getType()); // Check freeform group entries - assertEquals(2, freeformGroup.getTaskInfoList().size()); + assertEquals(3, freeformGroup.getTaskInfoList().size()); assertEquals(t1, freeformGroup.getTaskInfoList().get(0)); - assertEquals(t5, freeformGroup.getTaskInfoList().get(1)); + assertEquals(t3, freeformGroup.getTaskInfoList().get(1)); + assertEquals(t5, freeformGroup.getTaskInfoList().get(2)); // Check single entries assertEquals(t2, singleGroup1.getTaskInfo1()); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java new file mode 100644 index 000000000000..769acf7fdfde --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java @@ -0,0 +1,177 @@ +/* + * 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.recents; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityTaskManager; +import android.app.IApplicationThread; +import android.app.KeyguardManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +import android.platform.test.flag.junit.SetFlagsRule; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.HomeTransitionObserver; +import com.android.wm.shell.transition.Transitions; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.quality.Strictness; + +import java.util.Optional; + +/** + * Tests for {@link RecentTasksController} + * + * Usage: atest WMShellUnitTests:RecentsTransitionHandlerTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class RecentsTransitionHandlerTest extends ShellTestCase { + + @Mock + private Context mContext; + @Mock + private TaskStackListenerImpl mTaskStackListener; + @Mock + private ShellCommandHandler mShellCommandHandler; + @Mock + private DesktopModeTaskRepository mDesktopModeTaskRepository; + @Mock + private ActivityTaskManager mActivityTaskManager; + @Mock + private DisplayInsetsController mDisplayInsetsController; + @Mock + private IRecentTasksListener mRecentTasksListener; + @Mock + private TaskStackTransitionObserver mTaskStackTransitionObserver; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private ShellTaskOrganizer mShellTaskOrganizer; + private RecentTasksController mRecentTasksController; + private RecentTasksController mRecentTasksControllerReal; + private RecentsTransitionHandler mRecentsTransitionHandler; + private ShellInit mShellInit; + private ShellController mShellController; + private TestShellExecutor mMainExecutor; + private static StaticMockitoSession sMockitoSession; + + @Before + public void setUp() { + sMockitoSession = mockitoSession().initMocks(this).strictness(Strictness.LENIENT) + .mockStatic(DesktopModeStatus.class).startMocking(); + ExtendedMockito.doReturn(true) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + + mMainExecutor = new TestShellExecutor(); + when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class)); + when(mContext.getSystemService(KeyguardManager.class)) + .thenReturn(mock(KeyguardManager.class)); + mShellInit = spy(new ShellInit(mMainExecutor)); + mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler, + mDisplayInsetsController, mMainExecutor)); + mRecentTasksControllerReal = new RecentTasksController(mContext, mShellInit, + mShellController, mShellCommandHandler, mTaskStackListener, mActivityTaskManager, + Optional.of(mDesktopModeTaskRepository), mTaskStackTransitionObserver, + mMainExecutor); + mRecentTasksController = spy(mRecentTasksControllerReal); + mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit, mShellCommandHandler, + null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController), + mMainExecutor); + + final Transitions transitions = mock(Transitions.class); + doReturn(mMainExecutor).when(transitions).getMainExecutor(); + mRecentsTransitionHandler = new RecentsTransitionHandler(mShellInit, mShellTaskOrganizer, + transitions, mRecentTasksController, mock(HomeTransitionObserver.class)); + + mShellInit.init(); + } + + @After + public void tearDown() { + sMockitoSession.finishMocking(); + } + + @Test + public void testStartSyntheticRecentsTransition_callsOnAnimationStart() throws Exception { + final IRecentsAnimationRunner runner = mock(IRecentsAnimationRunner.class); + doReturn(new Binder()).when(runner).asBinder(); + Bundle options = new Bundle(); + options.putBoolean("is_synthetic_recents_transition", true); + IBinder transition = mRecentsTransitionHandler.startRecentsTransition( + mock(PendingIntent.class), new Intent(), options, mock(IApplicationThread.class), + runner); + verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any()); + + // Finish and verify no transition remains + mRecentsTransitionHandler.findController(transition).finish(true /* toHome */, + false /* sendUserLeaveHint */, null /* finishCb */); + mMainExecutor.flushAll(); + assertNull(mRecentsTransitionHandler.findController(transition)); + } + + @Test + public void testStartSyntheticRecentsTransition_callsOnAnimationCancel() throws Exception { + final IRecentsAnimationRunner runner = mock(IRecentsAnimationRunner.class); + doReturn(new Binder()).when(runner).asBinder(); + Bundle options = new Bundle(); + options.putBoolean("is_synthetic_recents_transition", true); + IBinder transition = mRecentsTransitionHandler.startRecentsTransition( + mock(PendingIntent.class), new Intent(), options, mock(IApplicationThread.class), + runner); + verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any()); + + mRecentsTransitionHandler.findController(transition).cancel("test"); + mMainExecutor.flushAll(); + verify(runner).onAnimationCanceled(any(), any()); + assertNull(mRecentsTransitionHandler.findController(transition)); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java index b790aee6fb0e..248393cef9ae 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java @@ -1,6 +1,6 @@ package com.android.wm.shell.recents; -import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -12,7 +12,7 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.util.SplitBounds; +import com.android.wm.shell.shared.split.SplitBounds; import org.junit.Before; import org.junit.Test; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt index f9599702e763..0e5efa650cc4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt @@ -48,7 +48,6 @@ import org.mockito.kotlin.same import org.mockito.kotlin.verify import org.mockito.kotlin.whenever - /** * Test class for {@link TaskStackTransitionObserver} * @@ -168,6 +167,80 @@ class TaskStackTransitionObserverTest { .isEqualTo(freeformOpenChange.taskInfo?.windowingMode) } + @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + fun transitionMerged_withChange_onlyOpenChangeIsNotified() { + val listener = TestListener() + val executor = TestShellExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + + // Create open transition + val change = + createChange( + WindowManager.TRANSIT_OPEN, + createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM) + ) + val transitionInfo = + TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build() + + // create change transition to be merged to above transition + val mergedChange = + createChange( + WindowManager.TRANSIT_CHANGE, + createTaskInfo(2, WindowConfiguration.WINDOWING_MODE_FREEFORM) + ) + val mergedTransitionInfo = + TransitionInfoBuilder(WindowManager.TRANSIT_CHANGE, 0).addChange(mergedChange).build() + val mergedTransition = Mockito.mock(IBinder::class.java) + + callOnTransitionReady(transitionInfo) + callOnTransitionReady(mergedTransitionInfo, mergedTransition) + callOnTransitionMerged(mergedTransition) + callOnTransitionFinished() + executor.flushAll() + + assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(change.taskInfo?.taskId) + assertThat(listener.taskInfoToBeNotified.windowingMode) + .isEqualTo(change.taskInfo?.windowingMode) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + fun transitionMerged_withOpen_lastOpenChangeIsNotified() { + val listener = TestListener() + val executor = TestShellExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + + // Create open transition + val change = + createChange( + WindowManager.TRANSIT_OPEN, + createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM) + ) + val transitionInfo = + TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build() + + // create change transition to be merged to above transition + val mergedChange = + createChange( + WindowManager.TRANSIT_OPEN, + createTaskInfo(2, WindowConfiguration.WINDOWING_MODE_FREEFORM) + ) + val mergedTransitionInfo = + TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(mergedChange).build() + val mergedTransition = Mockito.mock(IBinder::class.java) + + callOnTransitionReady(transitionInfo) + callOnTransitionReady(mergedTransitionInfo, mergedTransition) + callOnTransitionMerged(mergedTransition) + callOnTransitionFinished() + executor.flushAll() + + assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(mergedChange.taskInfo?.taskId) + assertThat(listener.taskInfoToBeNotified.windowingMode) + .isEqualTo(mergedChange.taskInfo?.windowingMode) + } + class TestListener : TaskStackTransitionObserver.TaskStackTransitionObserverListener { var taskInfoToBeNotified = ActivityManager.RunningTaskInfo() @@ -179,11 +252,14 @@ class TaskStackTransitionObserverTest { } /** Simulate calling the onTransitionReady() method */ - private fun callOnTransitionReady(transitionInfo: TransitionInfo) { + private fun callOnTransitionReady( + transitionInfo: TransitionInfo, + transition: IBinder = mockTransitionBinder + ) { val startT = Mockito.mock(SurfaceControl.Transaction::class.java) val finishT = Mockito.mock(SurfaceControl.Transaction::class.java) - transitionObserver.onTransitionReady(mockTransitionBinder, transitionInfo, startT, finishT) + transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT) } /** Simulate calling the onTransitionFinished() method */ @@ -191,6 +267,11 @@ class TaskStackTransitionObserverTest { transitionObserver.onTransitionFinished(mockTransitionBinder, false) } + /** Simulate calling the onTransitionMerged() method */ + private fun callOnTransitionMerged(merged: IBinder, playing: IBinder = mockTransitionBinder) { + transitionObserver.onTransitionMerged(merged, playing) + } + companion object { fun createTaskInfo(taskId: Int, windowingMode: Int): ActivityManager.RunningTaskInfo { val taskInfo = ActivityManager.RunningTaskInfo() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleBarLocationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/BubbleBarLocationTest.kt index 27e0b196f0be..b9bf95b16e70 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleBarLocationTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/BubbleBarLocationTest.kt @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.wm.shell.common.bubbles +package com.android.wm.shell.shared.bubbles import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTestCase -import com.android.wm.shell.common.bubbles.BubbleBarLocation.DEFAULT -import com.android.wm.shell.common.bubbles.BubbleBarLocation.LEFT -import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT +import com.android.wm.shell.shared.bubbles.BubbleBarLocation.DEFAULT +import com.android.wm.shell.shared.bubbles.BubbleBarLocation.LEFT +import com.android.wm.shell.shared.bubbles.BubbleBarLocation.RIGHT import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/BubbleInfoTest.kt index 5b22eddcb6ee..641063c27076 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/BubbleInfoTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles +package com.android.wm.shell.shared.bubbles import android.os.Parcel import android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE @@ -41,6 +41,7 @@ class BubbleInfoTest : ShellTestCase() { "com.some.package", "title", "Some app", + true, true ) val parcel = Parcel.obtain() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/handles/RegionSamplingHelperTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/handles/RegionSamplingHelperTest.kt new file mode 100644 index 000000000000..d3e291f7dd1f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/handles/RegionSamplingHelperTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.handles + + +import android.graphics.Rect +import android.testing.TestableLooper.RunWithLooper +import android.view.SurfaceControl +import android.view.View +import android.view.ViewRootImpl +import androidx.concurrent.futures.DirectExecutor +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestShellExecutor +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.argumentCaptor + +@RunWith(AndroidJUnit4::class) +@SmallTest +@RunWithLooper +class RegionSamplingHelperTest : ShellTestCase() { + + @Mock + lateinit var sampledView: View + @Mock + lateinit var samplingCallback: RegionSamplingHelper.SamplingCallback + @Mock + lateinit var compositionListener: RegionSamplingHelper.SysuiCompositionSamplingListener + @Mock + lateinit var viewRootImpl: ViewRootImpl + @Mock + lateinit var surfaceControl: SurfaceControl + @Mock + lateinit var wrappedSurfaceControl: SurfaceControl + @JvmField @Rule + var rule = MockitoJUnit.rule() + lateinit var regionSamplingHelper: RegionSamplingHelper + + @Before + fun setup() { + whenever(sampledView.isAttachedToWindow).thenReturn(true) + whenever(sampledView.viewRootImpl).thenReturn(viewRootImpl) + whenever(viewRootImpl.surfaceControl).thenReturn(surfaceControl) + whenever(surfaceControl.isValid).thenReturn(true) + whenever(wrappedSurfaceControl.isValid).thenReturn(true) + whenever(samplingCallback.isSamplingEnabled).thenReturn(true) + getInstrumentation().runOnMainSync(Runnable { + regionSamplingHelper = object : RegionSamplingHelper( + sampledView, samplingCallback, + DirectExecutor.INSTANCE, DirectExecutor.INSTANCE, compositionListener + ) { + override fun wrap(stopLayerControl: SurfaceControl?): SurfaceControl { + return wrappedSurfaceControl + } + } + }) + regionSamplingHelper.setWindowVisible(true) + } + + @Test + fun testStart_register() { + regionSamplingHelper.start(Rect(0, 0, 100, 100)) + verify(compositionListener).register(any(), anyInt(), eq(wrappedSurfaceControl), any()) + } + + @Test + fun testStart_unregister() { + regionSamplingHelper.start(Rect(0, 0, 100, 100)) + regionSamplingHelper.setWindowVisible(false) + verify(compositionListener).unregister(any()) + } + + @Test + fun testStart_hasBlur_neverRegisters() { + regionSamplingHelper.setWindowHasBlurs(true) + regionSamplingHelper.start(Rect(0, 0, 100, 100)) + verify(compositionListener, never()) + .register(any(), anyInt(), eq(wrappedSurfaceControl), any()) + } + + @Test + fun testStart_stopAndDestroy() { + regionSamplingHelper.start(Rect(0, 0, 100, 100)) + regionSamplingHelper.stopAndDestroy() + verify(compositionListener).unregister(any()) + } + + @Test + fun testCompositionSamplingListener_has_nonEmptyRect() { + // simulate race condition + val fakeExecutor = TestShellExecutor() // pass in as backgroundExecutor + val fakeSamplingCallback = mock(RegionSamplingHelper.SamplingCallback::class.java) + + whenever(fakeSamplingCallback.isSamplingEnabled).thenReturn(true) + whenever(wrappedSurfaceControl.isValid).thenReturn(true) + getInstrumentation().runOnMainSync(Runnable { + regionSamplingHelper = object : RegionSamplingHelper( + sampledView, fakeSamplingCallback, + DirectExecutor.INSTANCE, fakeExecutor, compositionListener + ) { + override fun wrap(stopLayerControl: SurfaceControl?): SurfaceControl { + return wrappedSurfaceControl + } + } + }) + regionSamplingHelper.setWindowVisible(true) + regionSamplingHelper.start(Rect(0, 0, 100, 100)) + + // make sure background task is enqueued + assertThat(fakeExecutor.getCallbacks().size).isEqualTo(1) + + // make sure regionSamplingHelper will have empty Rect + whenever(fakeSamplingCallback.getSampledRegion(any())).thenReturn(Rect(0, 0, 0, 0)) + regionSamplingHelper.onLayoutChange(sampledView, 0, 0, 0, 0, 0, 0, 0, 0) + + // resume running of background thread + fakeExecutor.flushAll() + + // grab Rect passed into compositionSamplingListener and make sure it's not empty + val argumentGrabber = argumentCaptor<Rect>() + verify(compositionListener).register(any(), anyInt(), eq(wrappedSurfaceControl), + argumentGrabber.capture()) + assertThat(argumentGrabber.firstValue.isEmpty).isFalse() + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/magnetictarget/MagnetizedObjectTest.kt index 8bb182de7668..8711ee01601c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/magnetictarget/MagnetizedObjectTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.wm.shell.common.magnetictarget +package com.android.wm.shell.shared.magnetictarget import android.testing.AndroidTestingRunner import android.testing.TestableLooper @@ -33,13 +33,13 @@ import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyFloat import org.mockito.Mockito -import org.mockito.Mockito.`when` import org.mockito.Mockito.doAnswer import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.`when` @TestableLooper.RunWithLooper @RunWith(AndroidTestingRunner::class) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitScreenConstantsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt index fe261107d65b..19c18be44ab1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitScreenConstantsTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.common.split import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.wm.shell.shared.split.SplitScreenConstants import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java deleted file mode 100644 index b1befc46f383..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.splitscreen; - -import static android.view.Display.DEFAULT_DISPLAY; - -import static com.google.common.truth.Truth.assertThat; - -import android.app.ActivityManager; -import android.view.SurfaceControl; -import android.view.SurfaceSession; -import android.window.WindowContainerTransaction; - -import androidx.test.annotation.UiThreadTest; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.launcher3.icons.IconProvider; -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; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.Optional; - -/** Tests for {@link MainStage} */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class MainStageTests extends ShellTestCase { - @Mock private ShellTaskOrganizer mTaskOrganizer; - @Mock private StageTaskListener.StageListenerCallbacks mCallbacks; - @Mock private SyncTransactionQueue mSyncQueue; - @Mock private ActivityManager.RunningTaskInfo mRootTaskInfo; - @Mock private SurfaceControl mRootLeash; - @Mock private IconProvider mIconProvider; - private WindowContainerTransaction mWct = new WindowContainerTransaction(); - private SurfaceSession mSurfaceSession = new SurfaceSession(); - private MainStage mMainStage; - - @Before - @UiThreadTest - public void setup() { - MockitoAnnotations.initMocks(this); - mRootTaskInfo = new TestRunningTaskInfoBuilder().build(); - mMainStage = new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, - mSyncQueue, mSurfaceSession, mIconProvider, Optional.empty()); - mMainStage.onTaskAppeared(mRootTaskInfo, mRootLeash); - } - - @Test - public void testActiveDeactivate() { - mMainStage.activate(mWct, true /* reparent */); - assertThat(mMainStage.isActive()).isTrue(); - - mMainStage.deactivate(mWct); - assertThat(mMainStage.isActive()).isFalse(); - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/OWNERS new file mode 100644 index 000000000000..9d926b26b149 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 928697 +# includes OWNERS from parent directories
\ No newline at end of file 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 deleted file mode 100644 index 549bd3fcabfb..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.splitscreen; - -import static android.view.Display.DEFAULT_DISPLAY; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.verify; - -import android.app.ActivityManager; -import android.view.SurfaceControl; -import android.view.SurfaceSession; -import android.window.WindowContainerTransaction; - -import androidx.test.annotation.UiThreadTest; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.launcher3.icons.IconProvider; -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; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.Spy; - -import java.util.Optional; - -/** Tests for {@link SideStage} */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class SideStageTests extends ShellTestCase { - @Mock private ShellTaskOrganizer mTaskOrganizer; - @Mock private StageTaskListener.StageListenerCallbacks mCallbacks; - @Mock private SyncTransactionQueue mSyncQueue; - @Mock private ActivityManager.RunningTaskInfo mRootTask; - @Mock private SurfaceControl mRootLeash; - @Mock private IconProvider mIconProvider; - @Spy private WindowContainerTransaction mWct; - private SurfaceSession mSurfaceSession = new SurfaceSession(); - private SideStage mSideStage; - - @Before - @UiThreadTest - public void setup() { - MockitoAnnotations.initMocks(this); - mRootTask = new TestRunningTaskInfoBuilder().build(); - mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, - mSyncQueue, mSurfaceSession, mIconProvider, Optional.empty()); - mSideStage.onTaskAppeared(mRootTask, mRootLeash); - } - - @Test - public void testAddTask() { - final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); - - mSideStage.addTask(task, mWct); - - verify(mWct).reparent(eq(task.token), eq(mRootTask.token), eq(true)); - } - - @Test - public void testRemoveTask() { - final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); - assertThat(mSideStage.removeTask(task.taskId, null, mWct)).isFalse(); - - mSideStage.mChildrenTaskInfo.put(task.taskId, task); - assertThat(mSideStage.removeTask(task.taskId, null, mWct)).isTrue(); - verify(mWct).reparent(eq(task.token), isNull(), eq(false)); - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java index 3c387f0d7c34..9260a07fd945 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -23,8 +23,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static org.junit.Assert.assertEquals; import static org.junit.Assume.assumeTrue; @@ -36,6 +36,7 @@ import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -49,6 +50,9 @@ import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; import android.os.Bundle; +import android.os.Handler; +import android.window.IWindowContainerToken; +import android.window.WindowContainerToken; import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -66,14 +70,14 @@ import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.shared.ShellSharedConstants; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; -import com.android.wm.shell.sysui.ShellSharedConstants; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.WindowDecorViewModel; @@ -100,6 +104,7 @@ public class SplitScreenControllerTests extends ShellTestCase { @Mock SyncTransactionQueue mSyncQueue; @Mock RootTaskDisplayAreaOrganizer mRootTDAOrganizer; @Mock ShellExecutor mMainExecutor; + @Mock Handler mMainHandler; @Mock DisplayController mDisplayController; @Mock DisplayImeController mDisplayImeController; @Mock DisplayInsetsController mDisplayInsetsController; @@ -130,7 +135,7 @@ public class SplitScreenControllerTests extends ShellTestCase { mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool, mIconProvider, Optional.of(mRecentTasks), mLaunchAdjacentController, Optional.of(mWindowDecorViewModel), Optional.of(mDesktopTasksController), - mStageCoordinator, mMultiInstanceHelper, mMainExecutor)); + mStageCoordinator, mMultiInstanceHelper, mMainExecutor, mMainHandler)); } @Test @@ -195,10 +200,10 @@ public class SplitScreenControllerTests extends ShellTestCase { PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, - SPLIT_POSITION_TOP_OR_LEFT, null); + SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */); verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(), - eq(SPLIT_POSITION_TOP_OR_LEFT), isNull()); + eq(SPLIT_POSITION_TOP_OR_LEFT), isNull(), isNull()); assertEquals(FLAG_ACTIVITY_NO_USER_ACTION, mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_NO_USER_ACTION); } @@ -213,19 +218,20 @@ public class SplitScreenControllerTests extends ShellTestCase { ActivityManager.RunningTaskInfo topRunningTask = createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent); doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask(); + doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask(any()); mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, - SPLIT_POSITION_TOP_OR_LEFT, null); + SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */); verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(), - eq(SPLIT_POSITION_TOP_OR_LEFT), isNull()); + eq(SPLIT_POSITION_TOP_OR_LEFT), isNull(), isNull()); assertEquals(FLAG_ACTIVITY_MULTIPLE_TASK, mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK); } @Test public void startIntent_multiInstancesNotSupported_startTaskInBackgroundBeforeSplitActivated() { - doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any()); + doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any(), any()); Intent startIntent = createStartIntent("startActivity"); PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); @@ -233,15 +239,16 @@ public class SplitScreenControllerTests extends ShellTestCase { ActivityManager.RunningTaskInfo topRunningTask = createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent); doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask(); + doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask(any()); // Put the same component into a task in the background ActivityManager.RecentTaskInfo sameTaskInfo = new ActivityManager.RecentTaskInfo(); - doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any(), anyInt()); + doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any(), anyInt(), any()); mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, - SPLIT_POSITION_TOP_OR_LEFT, null); + SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */); verify(mStageCoordinator).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT), - isNull()); + isNull(), isNull()); verify(mMultiInstanceHelper, never()).supportsMultiInstanceSplit(any()); verify(mStageCoordinator, never()).switchSplitPosition(any()); } @@ -249,7 +256,7 @@ public class SplitScreenControllerTests extends ShellTestCase { @Test public void startIntent_multiInstancesSupported_startTaskInBackgroundAfterSplitActivated() { doReturn(true).when(mMultiInstanceHelper).supportsMultiInstanceSplit(any()); - doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any()); + doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any(), any()); Intent startIntent = createStartIntent("startActivity"); PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); @@ -261,13 +268,13 @@ public class SplitScreenControllerTests extends ShellTestCase { SPLIT_POSITION_BOTTOM_OR_RIGHT); // Put the same component into a task in the background doReturn(new ActivityManager.RecentTaskInfo()).when(mRecentTasks) - .findTaskInBackground(any(), anyInt()); + .findTaskInBackground(any(), anyInt(), any()); mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, - SPLIT_POSITION_TOP_OR_LEFT, null); + SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */); verify(mMultiInstanceHelper, never()).supportsMultiInstanceSplit(any()); verify(mStageCoordinator).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT), - isNull()); + isNull(), isNull()); } @Test @@ -284,7 +291,7 @@ public class SplitScreenControllerTests extends ShellTestCase { SPLIT_POSITION_BOTTOM_OR_RIGHT); mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, - SPLIT_POSITION_TOP_OR_LEFT, null); + SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */); verify(mStageCoordinator).switchSplitPosition(anyString()); } @@ -312,6 +319,7 @@ public class SplitScreenControllerTests extends ShellTestCase { info.supportsMultiWindow = true; info.baseIntent = strIntent; info.baseActivity = strIntent.getComponent(); + info.token = new WindowContainerToken(mock(IWindowContainerToken.class)); ActivityInfo activityInfo = new ActivityInfo(); activityInfo.packageName = info.baseActivity.getPackageName(); activityInfo.name = info.baseActivity.getClassName(); 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 a3009a55198f..66dcef6f14cc 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 @@ -22,8 +22,8 @@ import static org.mockito.Mockito.mock; import android.app.ActivityManager; import android.content.Context; import android.graphics.Rect; +import android.os.Handler; import android.view.SurfaceControl; -import android.view.SurfaceSession; import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.wm.shell.ShellTaskOrganizer; @@ -34,9 +34,9 @@ import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.split.SplitLayout; import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.WindowDecorViewModel; @@ -73,22 +73,22 @@ public class SplitTestUtils { final SurfaceControl mRootLeash; TestStageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, - ShellTaskOrganizer taskOrganizer, MainStage mainStage, SideStage sideStage, - DisplayController displayController, DisplayImeController imeController, - DisplayInsetsController insetsController, SplitLayout splitLayout, - Transitions transitions, TransactionPool transactionPool, - ShellExecutor mainExecutor, + ShellTaskOrganizer taskOrganizer, StageTaskListener mainStage, + StageTaskListener sideStage, DisplayController displayController, + DisplayImeController imeController, DisplayInsetsController insetsController, + SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool, + ShellExecutor mainExecutor, Handler mainHandler, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, Optional<WindowDecorViewModel> windowDecorViewModel) { super(context, displayId, syncQueue, taskOrganizer, mainStage, sideStage, displayController, imeController, insetsController, splitLayout, - transitions, transactionPool, mainExecutor, recentTasks, + transitions, transactionPool, mainExecutor, mainHandler, recentTasks, launchAdjacentController, windowDecorViewModel); // Prepare root task for testing. mRootTask = new TestRunningTaskInfoBuilder().build(); - mRootLeash = new SurfaceControl.Builder(new SurfaceSession()).setName("test").build(); + mRootLeash = new SurfaceControl.Builder().setName("test").build(); onTaskAppeared(mRootTask, mRootLeash); } } 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 ae226b807d13..ce3944a5855e 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 @@ -49,10 +49,10 @@ import static org.mockito.Mockito.verify; import android.annotation.NonNull; import android.app.ActivityManager; +import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.window.IRemoteTransition; import android.window.RemoteTransition; import android.window.TransitionInfo; @@ -75,9 +75,9 @@ import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.split.SplitDecorManager; import com.android.wm.shell.common.split.SplitLayout; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.transition.DefaultMixedHandler; import com.android.wm.shell.transition.TestRemoteTransition; import com.android.wm.shell.transition.TransitionInfoBuilder; @@ -105,17 +105,17 @@ public class SplitTransitionTests extends ShellTestCase { @Mock private DisplayInsetsController mDisplayInsetsController; @Mock private TransactionPool mTransactionPool; @Mock private Transitions mTransitions; - @Mock private SurfaceSession mSurfaceSession; @Mock private IconProvider mIconProvider; @Mock private WindowDecorViewModel mWindowDecorViewModel; @Mock private ShellExecutor mMainExecutor; + @Mock private Handler mMainHandler; @Mock private LaunchAdjacentController mLaunchAdjacentController; @Mock private DefaultMixedHandler mMixedHandler; @Mock private SplitScreen.SplitInvocationListener mInvocationListener; private final TestShellExecutor mTestShellExecutor = new TestShellExecutor(); private SplitLayout mSplitLayout; - private MainStage mMainStage; - private SideStage mSideStage; + private StageTaskListener mMainStage; + private StageTaskListener mSideStage; private StageCoordinator mStageCoordinator; private SplitScreenTransitions mSplitScreenTransitions; @@ -131,18 +131,18 @@ public class SplitTransitionTests extends ShellTestCase { doReturn(mockExecutor).when(mTransitions).getAnimExecutor(); doReturn(mock(SurfaceControl.Transaction.class)).when(mTransactionPool).acquire(); mSplitLayout = SplitTestUtils.createMockSplitLayout(); - mMainStage = spy(new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( - StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession, + mMainStage = spy(new StageTaskListener(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( + StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mIconProvider, Optional.of(mWindowDecorViewModel))); mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface()); - mSideStage = spy(new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( - StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession, + mSideStage = spy(new StageTaskListener(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( + StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mIconProvider, Optional.of(mWindowDecorViewModel))); mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface()); mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions, - mTransactionPool, mMainExecutor, Optional.empty(), + mTransactionPool, mMainExecutor, mMainHandler, Optional.empty(), mLaunchAdjacentController, Optional.empty()); mStageCoordinator.setMixedHandler(mMixedHandler); mSplitScreenTransitions = mStageCoordinator.getSplitTransitions(); @@ -217,7 +217,7 @@ public class SplitTransitionTests extends ShellTestCase { @Test @UiThreadTest - public void testRemoteTransitionConsumed() { + public void testRemoteTransitionConsumedForStartAnimation() { // Omit side child change TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) .addChange(TRANSIT_OPEN, mMainChild) @@ -236,7 +236,30 @@ public class SplitTransitionTests extends ShellTestCase { assertTrue(accepted); assertTrue(testRemote.isConsumed()); + } + + @Test + @UiThreadTest + public void testRemoteTransitionConsumed() { + // Omit side child change + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) + .addChange(TRANSIT_OPEN, mMainChild) + .build(); + TestRemoteTransition testRemote = new TestRemoteTransition(); + IBinder transition = mSplitScreenTransitions.startEnterTransition( + TRANSIT_OPEN, new WindowContainerTransaction(), + new RemoteTransition(testRemote, "Test"), mStageCoordinator, + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false); + mMainStage.onTaskAppeared(mMainChild, createMockSurface()); + mStageCoordinator.startAnimation(transition, info, + mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), + mock(Transitions.TransitionFinishCallback.class)); + mStageCoordinator.onTransitionConsumed(transition, false /*aborted*/, + mock(SurfaceControl.Transaction.class)); + + assertTrue(testRemote.isConsumed()); } @Test 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 d18fec2f24ad..a6c16c43c8cb 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 @@ -19,13 +19,12 @@ package com.android.wm.shell.splitscreen; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.view.Display.DEFAULT_DISPLAY; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.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 static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME; import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS; import static com.google.common.truth.Truth.assertThat; @@ -51,7 +50,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.window.RemoteTransition; import android.window.WindowContainerTransaction; @@ -69,9 +67,9 @@ import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.split.SplitDecorManager; import com.android.wm.shell.common.split.SplitLayout; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; @@ -98,9 +96,9 @@ public class StageCoordinatorTests extends ShellTestCase { @Mock private SyncTransactionQueue mSyncQueue; @Mock - private MainStage mMainStage; + private StageTaskListener mMainStage; @Mock - private SideStage mSideStage; + private StageTaskListener mSideStage; @Mock private SplitLayout mSplitLayout; @Mock @@ -120,7 +118,6 @@ public class StageCoordinatorTests extends ShellTestCase { private final Rect mBounds2 = new Rect(5, 10, 15, 20); private final Rect mRootBounds = new Rect(0, 0, 45, 60); - private SurfaceSession mSurfaceSession = new SurfaceSession(); private SurfaceControl mRootLeash; private SurfaceControl mDividerLeash; private ActivityManager.RunningTaskInfo mRootTask; @@ -138,8 +135,9 @@ public class StageCoordinatorTests extends ShellTestCase { mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool, - mMainExecutor, Optional.empty(), mLaunchAdjacentController, Optional.empty())); - mDividerLeash = new SurfaceControl.Builder(mSurfaceSession).setName("fakeDivider").build(); + mMainExecutor, mMainHandler, Optional.empty(), mLaunchAdjacentController, + Optional.empty())); + mDividerLeash = new SurfaceControl.Builder().setName("fakeDivider").build(); when(mSplitLayout.getBounds1()).thenReturn(mBounds1); when(mSplitLayout.getBounds2()).thenReturn(mBounds2); @@ -149,7 +147,7 @@ public class StageCoordinatorTests extends ShellTestCase { when(mSplitLayout.getDividerLeash()).thenReturn(mDividerLeash); mRootTask = new TestRunningTaskInfoBuilder().build(); - mRootLeash = new SurfaceControl.Builder(mSurfaceSession).setName("test").build(); + mRootLeash = new SurfaceControl.Builder().setName("test").build(); mStageCoordinator.onTaskAppeared(mRootTask, mRootLeash); mSideStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build(); @@ -244,38 +242,6 @@ public class StageCoordinatorTests extends ShellTestCase { } @Test - public void testExitSplitScreen() { - when(mMainStage.isActive()).thenReturn(true); - mStageCoordinator.exitSplitScreen(INVALID_TASK_ID, EXIT_REASON_RETURN_HOME); - verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(false)); - verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false)); - } - - @Test - public void testExitSplitScreenToMainStage() { - when(mMainStage.isActive()).thenReturn(true); - final int testTaskId = 12345; - when(mMainStage.containsTask(eq(testTaskId))).thenReturn(true); - when(mSideStage.containsTask(eq(testTaskId))).thenReturn(false); - mStageCoordinator.exitSplitScreen(testTaskId, EXIT_REASON_RETURN_HOME); - verify(mMainStage).reorderChild(eq(testTaskId), eq(true), - any(WindowContainerTransaction.class)); - verify(mMainStage).resetBounds(any(WindowContainerTransaction.class)); - } - - @Test - public void testExitSplitScreenToSideStage() { - when(mMainStage.isActive()).thenReturn(true); - final int testTaskId = 12345; - when(mMainStage.containsTask(eq(testTaskId))).thenReturn(false); - when(mSideStage.containsTask(eq(testTaskId))).thenReturn(true); - mStageCoordinator.exitSplitScreen(testTaskId, EXIT_REASON_RETURN_HOME); - verify(mSideStage).reorderChild(eq(testTaskId), eq(true), - any(WindowContainerTransaction.class)); - verify(mSideStage).resetBounds(any(WindowContainerTransaction.class)); - } - - @Test public void testResolveStartStage_beforeSplitActivated_setsStagePosition() { mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null /* wct */); @@ -347,8 +313,7 @@ public class StageCoordinatorTests extends ShellTestCase { assertThat(options.getLaunchRootTask()).isEqualTo(mMainStage.mRootTaskInfo.token); assertThat(options.getPendingIntentBackgroundActivityStartMode()) - .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission()).isTrue(); + .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); } @Test @@ -359,19 +324,15 @@ public class StageCoordinatorTests extends ShellTestCase { mMainStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().setVisible(true).build(); when(mStageCoordinator.isSplitActive()).thenReturn(true); when(mStageCoordinator.isSplitScreenVisible()).thenReturn(true); + when(mStageCoordinator.willSleepOnFold()).thenReturn(true); mStageCoordinator.onFoldedStateChanged(true); - assertEquals(mStageCoordinator.mTopStageAfterFoldDismiss, STAGE_TYPE_MAIN); + assertEquals(mStageCoordinator.mLastActiveStage, STAGE_TYPE_MAIN); mStageCoordinator.onFinishedWakingUp(); - if (Transitions.ENABLE_SHELL_TRANSITIONS) { - verify(mTaskOrganizer).startNewTransition(eq(TRANSIT_SPLIT_DISMISS), notNull()); - } else { - verify(mStageCoordinator).onSplitScreenExit(); - verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false)); - } + verify(mTaskOrganizer).startNewTransition(eq(TRANSIT_SPLIT_DISMISS), notNull()); } @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 946a7ef7d8c3..b7b7d0d35bcf 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 @@ -25,13 +25,13 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; 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; import android.window.WindowContainerTransaction; import androidx.test.annotation.UiThreadTest; @@ -52,6 +52,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.Spy; import java.util.Optional; @@ -76,9 +77,10 @@ public final class StageTaskListenerTests extends ShellTestCase { private IconProvider mIconProvider; @Mock private WindowDecorViewModel mWindowDecorViewModel; + @Spy + private WindowContainerTransaction mWct; @Captor private ArgumentCaptor<SyncTransactionQueue.TransactionRunnable> mRunnableCaptor; - private SurfaceSession mSurfaceSession = new SurfaceSession(); private SurfaceControl mSurfaceControl; private ActivityManager.RunningTaskInfo mRootTask; private StageTaskListener mStageTaskListener; @@ -93,12 +95,11 @@ public final class StageTaskListenerTests extends ShellTestCase { DEFAULT_DISPLAY, mCallbacks, mSyncQueue, - mSurfaceSession, mIconProvider, Optional.of(mWindowDecorViewModel)); mRootTask = new TestRunningTaskInfoBuilder().build(); mRootTask.parentTaskId = INVALID_TASK_ID; - mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession).setName("test").build(); + mSurfaceControl = new SurfaceControl.Builder().setName("test").build(); mStageTaskListener.onTaskAppeared(mRootTask, mSurfaceControl); } @@ -177,4 +178,31 @@ public final class StageTaskListenerTests extends ShellTestCase { mStageTaskListener.evictAllChildren(wct); assertFalse(wct.isEmpty()); } + + @Test + public void testAddTask() { + final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); + mStageTaskListener.addTask(task, mWct); + + verify(mWct).reparent(eq(task.token), eq(mRootTask.token), eq(true)); + } + + @Test + public void testRemoveTask() { + final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); + assertThat(mStageTaskListener.removeTask(task.taskId, null, mWct)).isFalse(); + + mStageTaskListener.mChildrenTaskInfo.put(task.taskId, task); + assertThat(mStageTaskListener.removeTask(task.taskId, null, mWct)).isTrue(); + verify(mWct).reparent(eq(task.token), isNull(), eq(false)); + } + + @Test + public void testActiveDeactivate() { + mStageTaskListener.activate(mWct, true /* reparent */); + assertThat(mStageTaskListener.isActive()).isTrue(); + + mStageTaskListener.deactivate(mWct); + assertThat(mStageTaskListener.isActive()).isFalse(); + } } 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 ee9f88663326..5f7542332c80 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 @@ -74,7 +74,7 @@ import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.HandlerExecutor; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.shared.TransactionPool; import org.junit.Before; import org.junit.Test; @@ -370,6 +370,6 @@ public class StartingSurfaceDrawerTests extends ShellTestCase { Surface.ROTATION_0, taskSize, contentInsets, new Rect() /* letterboxInsets */, false, true /* isRealSnapshot */, WINDOWING_MODE_FULLSCREEN, 0 /* systemUiVisibility */, false /* isTranslucent */, - hasImeSurface /* hasImeSurface */); + hasImeSurface /* hasImeSurface */, 0 /* uiMode */); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java index ff76a2f13527..7fd1c11e61ae 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java @@ -42,11 +42,11 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.shared.ShellSharedConstants; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; -import com.android.wm.shell.sysui.ShellSharedConstants; import org.junit.Before; import org.junit.Test; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java index 0434742c571b..17fd95b69dba 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java @@ -49,7 +49,6 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.SurfaceControl; import android.view.SurfaceHolder; -import android.view.SurfaceSession; import android.view.ViewTreeObserver; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -95,7 +94,6 @@ public class TaskViewTest extends ShellTestCase { Looper mViewLooper; TestHandler mViewHandler; - SurfaceSession mSession; SurfaceControl mLeash; Context mContext; @@ -106,7 +104,7 @@ public class TaskViewTest extends ShellTestCase { @Before public void setUp() { MockitoAnnotations.initMocks(this); - mLeash = new SurfaceControl.Builder(mSession) + mLeash = new SurfaceControl.Builder() .setName("test") .build(); @@ -294,16 +292,6 @@ public class TaskViewTest extends ShellTestCase { } @Test - public void testUnsetOnBackPressedOnTaskRoot_legacyTransitions() { - assumeFalse(Transitions.ENABLE_SHELL_TRANSITIONS); - mTaskViewTaskController.onTaskAppeared(mTaskInfo, mLeash); - verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(true)); - - mTaskViewTaskController.onTaskVanished(mTaskInfo); - verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(false)); - } - - @Test public void testOnNewTask_noSurface() { assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); WindowContainerTransaction wct = new WindowContainerTransaction(); @@ -443,19 +431,6 @@ public class TaskViewTest extends ShellTestCase { } @Test - public void testUnsetOnBackPressedOnTaskRoot() { - assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS); - WindowContainerTransaction wct = new WindowContainerTransaction(); - mTaskViewTaskController.prepareOpenAnimation(true /* newTask */, - new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo, - mLeash, wct); - verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(true)); - - mTaskViewTaskController.prepareCloseAnimation(); - verify(mOrganizer).setInterceptBackPressedOnTaskRoot(eq(mTaskInfo.token), eq(false)); - } - - @Test public void testSetObscuredTouchRect() { mTaskView.setObscuredTouchRect( new Rect(/* left= */ 0, /* top= */ 10, /* right= */ 100, /* bottom= */ 120)); @@ -713,4 +688,26 @@ public class TaskViewTest extends ShellTestCase { verify(mViewHandler).post(any()); verify(mTaskView).setResizeBackgroundColor(eq(Color.BLUE)); } + + @Test + public void testOnAppeared_setsTrimmableTask() { + WindowContainerTransaction wct = new WindowContainerTransaction(); + mTaskViewTaskController.prepareOpenAnimation(true /* newTask */, + new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo, + mLeash, wct); + + assertThat(wct.getHierarchyOps().get(0).isTrimmableFromRecents()).isFalse(); + } + + @Test + public void testMoveToFullscreen_callsTaskRemovalStarted() { + WindowContainerTransaction wct = new WindowContainerTransaction(); + mTaskViewTaskController.prepareOpenAnimation(true /* newTask */, + new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo, + mLeash, wct); + mTaskView.surfaceCreated(mock(SurfaceHolder.class)); + mTaskViewTaskController.moveToFullscreen(); + + verify(mViewListener).onTaskRemovalStarted(eq(mTaskInfo.taskId)); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ChangeBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ChangeBuilder.java new file mode 100644 index 000000000000..b54c3bf72110 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ChangeBuilder.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2024 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.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; + +import static org.mockito.Mockito.mock; + +import android.app.ActivityManager.RunningTaskInfo; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; + +public class ChangeBuilder { + final TransitionInfo.Change mChange; + + ChangeBuilder(@WindowManager.TransitionType int mode) { + mChange = new TransitionInfo.Change(null /* token */, createMockSurface(true)); + 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; + } + + private static SurfaceControl createMockSurface(boolean valid) { + SurfaceControl sc = mock(SurfaceControl.class); + doReturn(valid).when(sc).isValid(); + return sc; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java new file mode 100644 index 000000000000..0c18229f38d0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2024 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.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_SLEEP; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.window.TransitionInfo.FLAG_SYNC; +import static android.window.TransitionInfo.FLAG_TRANSLUCENT; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.WindowContainerToken; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.shared.TransactionPool; +import com.android.wm.shell.sysui.ShellInit; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for the default animation handler that is used if no other special-purpose handler picks + * up an animation request. + * + * Build/Install/Run: + * atest WMShellUnitTests:DefaultTransitionHandlerTest + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class DefaultTransitionHandlerTest extends ShellTestCase { + + private final Context mContext = + InstrumentationRegistry.getInstrumentation().getTargetContext(); + + private final DisplayController mDisplayController = mock(DisplayController.class); + private final TransactionPool mTransactionPool = new MockTransactionPool(); + private final TestShellExecutor mMainExecutor = new TestShellExecutor(); + private final TestShellExecutor mAnimExecutor = new TestShellExecutor(); + private final Handler mMainHandler = new Handler(Looper.getMainLooper()); + + private ShellInit mShellInit; + private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; + private DefaultTransitionHandler mTransitionHandler; + + @Before + public void setUp() { + mShellInit = new ShellInit(mMainExecutor); + mRootTaskDisplayAreaOrganizer = new RootTaskDisplayAreaOrganizer( + mMainExecutor, + mContext, + mShellInit); + mTransitionHandler = new DefaultTransitionHandler( + mContext, mShellInit, mDisplayController, + mTransactionPool, mMainExecutor, mMainHandler, mAnimExecutor, + mRootTaskDisplayAreaOrganizer); + mShellInit.init(); + } + + @After + public void tearDown() { + flushHandlers(); + } + + private void flushHandlers() { + mMainHandler.runWithScissors(() -> { + mAnimExecutor.flushAll(); + mMainExecutor.flushAll(); + }, 1000L); + } + + @Test + public void testAnimationBackgroundCreatedForTaskTransition() { + final TransitionInfo.Change openTask = new ChangeBuilder(TRANSIT_OPEN) + .setTask(createTaskInfo(1)) + .build(); + final TransitionInfo.Change closeTask = new ChangeBuilder(TRANSIT_TO_BACK) + .setTask(createTaskInfo(2)) + .build(); + + final IBinder token = new Binder(); + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(openTask) + .addChange(closeTask) + .build(); + final SurfaceControl.Transaction startT = MockTransactionPool.create(); + final SurfaceControl.Transaction finishT = MockTransactionPool.create(); + + mTransitionHandler.startAnimation(token, info, startT, finishT, + mock(Transitions.TransitionFinishCallback.class)); + + mergeSync(mTransitionHandler, token); + flushHandlers(); + + verify(startT).setColor(any(), any()); + } + + @Test + public void testNoAnimationBackgroundForTranslucentTasks() { + final TransitionInfo.Change openTask = new ChangeBuilder(TRANSIT_OPEN) + .setTask(createTaskInfo(1)) + .setFlags(FLAG_TRANSLUCENT) + .build(); + final TransitionInfo.Change closeTask = new ChangeBuilder(TRANSIT_TO_BACK) + .setTask(createTaskInfo(2)) + .setFlags(FLAG_TRANSLUCENT) + .build(); + + final IBinder token = new Binder(); + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(openTask) + .addChange(closeTask) + .build(); + final SurfaceControl.Transaction startT = MockTransactionPool.create(); + final SurfaceControl.Transaction finishT = MockTransactionPool.create(); + + mTransitionHandler.startAnimation(token, info, startT, finishT, + mock(Transitions.TransitionFinishCallback.class)); + + mergeSync(mTransitionHandler, token); + flushHandlers(); + + verify(startT, never()).setColor(any(), any()); + } + + @Test + public void testNoAnimationBackgroundForWallpapers() { + final TransitionInfo.Change openWallpaper = new ChangeBuilder(TRANSIT_OPEN) + .setFlags(TransitionInfo.FLAG_IS_WALLPAPER) + .build(); + final TransitionInfo.Change closeWallpaper = new ChangeBuilder(TRANSIT_TO_BACK) + .setFlags(TransitionInfo.FLAG_IS_WALLPAPER) + .build(); + + final IBinder token = new Binder(); + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(openWallpaper) + .addChange(closeWallpaper) + .build(); + final SurfaceControl.Transaction startT = MockTransactionPool.create(); + final SurfaceControl.Transaction finishT = MockTransactionPool.create(); + + mTransitionHandler.startAnimation(token, info, startT, finishT, + mock(Transitions.TransitionFinishCallback.class)); + + mergeSync(mTransitionHandler, token); + flushHandlers(); + + verify(startT, never()).setColor(any(), any()); + } + + @Test + public void startAnimation_freeformOpenChange_doesntReparentTask() { + final TransitionInfo.Change openChange = new ChangeBuilder(TRANSIT_OPEN) + .setTask(createTaskInfo( + /* taskId= */ 1, /* windowingMode= */ WINDOWING_MODE_FULLSCREEN)) + .build(); + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(openChange) + .build(); + final IBinder token = new Binder(); + final SurfaceControl.Transaction startT = MockTransactionPool.create(); + final SurfaceControl.Transaction finishT = MockTransactionPool.create(); + + mTransitionHandler.startAnimation(token, info, startT, finishT, + mock(Transitions.TransitionFinishCallback.class)); + + verify(startT, never()).reparent(any(), any()); + } + + @Test + public void startAnimation_freeformMinimizeChange_underFullscreenChange_doesntReparentTask() { + final TransitionInfo.Change openChange = new ChangeBuilder(TRANSIT_OPEN) + .setTask(createTaskInfo( + /* taskId= */ 1, /* windowingMode= */ WINDOWING_MODE_FULLSCREEN)) + .build(); + final TransitionInfo.Change toBackChange = new ChangeBuilder(TRANSIT_TO_BACK) + .setTask(createTaskInfo( + /* taskId= */ 2, /* windowingMode= */ WINDOWING_MODE_FREEFORM)) + .build(); + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(openChange) + .addChange(toBackChange) + .build(); + final IBinder token = new Binder(); + final SurfaceControl.Transaction startT = MockTransactionPool.create(); + final SurfaceControl.Transaction finishT = MockTransactionPool.create(); + + mTransitionHandler.startAnimation(token, info, startT, finishT, + mock(Transitions.TransitionFinishCallback.class)); + + verify(startT, never()).reparent(any(), any()); + } + + @Test + public void startAnimation_freeform_minimizeAnimation_reparentsTask() { + final TransitionInfo.Change openChange = new ChangeBuilder(TRANSIT_OPEN) + .setTask(createTaskInfo( + /* taskId= */ 1, /* windowingMode= */ WINDOWING_MODE_FREEFORM)) + .build(); + final TransitionInfo.Change toBackChange = new ChangeBuilder(TRANSIT_TO_BACK) + .setTask(createTaskInfo( + /* taskId= */ 2, /* windowingMode= */ WINDOWING_MODE_FREEFORM)) + .build(); + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(openChange) + .addChange(toBackChange) + .build(); + final IBinder token = new Binder(); + final SurfaceControl.Transaction startT = MockTransactionPool.create(); + final SurfaceControl.Transaction finishT = MockTransactionPool.create(); + + mTransitionHandler.startAnimation(token, info, startT, finishT, + mock(Transitions.TransitionFinishCallback.class)); + + verify(startT).reparent(any(), any()); + } + + private static void mergeSync(Transitions.TransitionHandler handler, IBinder token) { + handler.mergeAnimation( + new Binder(), + new TransitionInfoBuilder(TRANSIT_SLEEP, FLAG_SYNC).build(), + MockTransactionPool.create(), + token, + mock(Transitions.TransitionFinishCallback.class)); + } + + private static RunningTaskInfo createTaskInfo(int taskId) { + return createTaskInfo(taskId, WINDOWING_MODE_FULLSCREEN); + } + + private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) { + RunningTaskInfo taskInfo = new RunningTaskInfo(); + taskInfo.taskId = taskId; + taskInfo.topActivityType = ACTIVITY_TYPE_STANDARD; + taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); + taskInfo.configuration.windowConfiguration.setActivityType(taskInfo.topActivityType); + taskInfo.token = mock(WindowContainerToken.class); + return taskInfo; + } +} + diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java index 0db10ef65a74..8f49de0a98fb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java @@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; @@ -38,6 +39,10 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.SurfaceControl; import android.window.TransitionInfo; import android.window.TransitionInfo.TransitionMode; @@ -46,17 +51,19 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; 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 com.android.wm.shell.shared.IHomeTransitionListener; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -70,6 +77,8 @@ import java.util.List; @RunWith(AndroidJUnit4.class) public class HomeTransitionObserverTest extends ShellTestCase { + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private final ShellTaskOrganizer mOrganizer = mock(ShellTaskOrganizer.class); private final TransactionPool mTransactionPool = mock(TransactionPool.class); private final Context mContext = @@ -187,6 +196,7 @@ public class HomeTransitionObserverTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_MIGRATE_PREDICTIVE_BACK_TRANSITION) public void testHomeActivityWithBackGestureNotifiesHomeIsVisible() throws RemoteException { TransitionInfo info = mock(TransitionInfo.class); TransitionInfo.Change change = mock(TransitionInfo.Change.class); @@ -205,6 +215,35 @@ public class HomeTransitionObserverTest extends ShellTestCase { verify(mListener, times(1)).onHomeVisibilityChanged(true); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_MIGRATE_PREDICTIVE_BACK_TRANSITION) + public void testHomeActivityWithBackGestureNotifiesHomeIsVisibleAfterClose() + throws RemoteException { + TransitionInfo info = mock(TransitionInfo.class); + TransitionInfo.Change change = mock(TransitionInfo.Change.class); + ActivityManager.RunningTaskInfo taskInfo = mock(ActivityManager.RunningTaskInfo.class); + when(change.getTaskInfo()).thenReturn(taskInfo); + when(info.getChanges()).thenReturn(new ArrayList<>(List.of(change))); + when(info.getType()).thenReturn(TRANSIT_PREPARE_BACK_NAVIGATION); + + when(change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)).thenReturn(true); + setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_OPEN, true); + + mHomeTransitionObserver.onTransitionReady(mock(IBinder.class), + info, + mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); + verify(mListener, times(0)).onHomeVisibilityChanged(anyBoolean()); + + when(info.getType()).thenReturn(TRANSIT_TO_BACK); + setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_CHANGE, true); + mHomeTransitionObserver.onTransitionReady(mock(IBinder.class), + info, + mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); + verify(mListener, times(1)).onHomeVisibilityChanged(true); + } + /** * Helper class to initialize variables for the rest. */ diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/MockTransactionPool.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/MockTransactionPool.java new file mode 100644 index 000000000000..a5a27e2568a3 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/MockTransactionPool.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 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 org.mockito.Mockito.RETURNS_SELF; +import static org.mockito.Mockito.mock; + +import android.view.SurfaceControl; + +import com.android.wm.shell.shared.TransactionPool; +import com.android.wm.shell.util.StubTransaction; + +public class MockTransactionPool extends TransactionPool { + + public static SurfaceControl.Transaction create() { + return mock(StubTransaction.class, RETURNS_SELF); + } + + @Override + public SurfaceControl.Transaction acquire() { + return create(); + } + + @Override + public void release(SurfaceControl.Transaction t) { + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/OWNERS new file mode 100644 index 000000000000..a24088a99de2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 316275 +# includes OWNERS from parent directories
\ No newline at end of file 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 964d86e8bd35..aea14b900647 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 @@ -76,10 +76,8 @@ import android.os.RemoteException; import android.platform.test.flag.junit.SetFlagsRule; import android.util.ArraySet; import android.util.Pair; -import android.view.IRecentsAnimationRunner; import android.view.Surface; import android.view.SurfaceControl; -import android.view.WindowManager; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; import android.window.IWindowContainerToken; @@ -108,12 +106,13 @@ import com.android.wm.shell.TestShellExecutor; 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.recents.IRecentsAnimationRunner; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.recents.RecentsTransitionHandler; +import com.android.wm.shell.shared.ShellSharedConstants; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; -import com.android.wm.shell.sysui.ShellSharedConstants; import com.android.wm.shell.util.StubTransaction; import org.junit.Before; @@ -333,6 +332,35 @@ public class ShellTransitionTests extends ShellTestCase { } @Test + public void testTransitionFilterTaskFragmentToken() { + final IBinder taskFragmentToken = new Binder(); + + TransitionFilter filter = new TransitionFilter(); + filter.mRequirements = + new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()}; + filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; + filter.mRequirements[0].mTaskFragmentToken = taskFragmentToken; + + // Transition with the same token should match. + final TransitionInfo infoHasTaskFragmentToken = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, taskFragmentToken).build(); + assertTrue(filter.matches(infoHasTaskFragmentToken)); + + // Transition with a different token should not match. + final IBinder differentTaskFragmentToken = new Binder(); + final TransitionInfo infoDifferentTaskFragmentToken = + new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, differentTaskFragmentToken).build(); + assertFalse(filter.matches(infoDifferentTaskFragmentToken)); + + // Transition without a token should not match. + final TransitionInfo infoNoTaskFragmentToken = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, createTaskInfo( + 1, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD)).build(); + assertFalse(filter.matches(infoNoTaskFragmentToken)); + } + + @Test public void testTransitionFilterMultiRequirement() { // filter that requires at-least one opening and one closing app TransitionFilter filter = new TransitionFilter(); @@ -443,6 +471,27 @@ public class ShellTransitionTests extends ShellTestCase { } @Test + public void testTransitionFilterAnimOverride() { + TransitionFilter filter = new TransitionFilter(); + filter.mRequirements = + new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()}; + filter.mRequirements[0].mCustomAnimation = true; + filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; + + final RunningTaskInfo taskInf = createTaskInfo(1); + final TransitionInfo openTask = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, taskInf).build(); + assertFalse(filter.matches(openTask)); + + final TransitionInfo.AnimationOptions overOpts = + TransitionInfo.AnimationOptions.makeCustomAnimOptions("pakname", 0, 0, 0, true); + final TransitionInfo openTaskOpts = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, taskInf).build(); + openTaskOpts.getChanges().get(0).setAnimationOptions(overOpts); + assertTrue(filter.matches(openTaskOpts)); + } + + @Test public void testRegisteredRemoteTransition() { Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); @@ -537,7 +586,7 @@ public class ShellTransitionTests extends ShellTestCase { mMainExecutor.flushAll(); // Takeover shouldn't happen when the flag is disabled. - setFlagsRule.disableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY); + setFlagsRule.disableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED); IBinder transitToken = new Binder(); transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); @@ -552,7 +601,7 @@ public class ShellTransitionTests extends ShellTestCase { verify(mOrganizer, times(1)).finishTransition(eq(transitToken), any()); // Takeover should happen when the flag is enabled. - setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY); + setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED); transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); info = new TransitionInfoBuilder(TRANSIT_OPEN) @@ -1191,7 +1240,7 @@ public class ShellTransitionTests extends ShellTestCase { mTransactionPool, createTestDisplayController(), mMainExecutor, mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); final RecentsTransitionHandler recentsHandler = - new RecentsTransitionHandler(shellInit, transitions, + new RecentsTransitionHandler(shellInit, mock(ShellTaskOrganizer.class), transitions, mock(RecentTasksController.class), mock(HomeTransitionObserver.class)); transitions.replaceDefaultHandlerForTest(mDefaultHandler); shellInit.init(); @@ -1614,43 +1663,6 @@ public class ShellTransitionTests extends ShellTestCase { eq(R.styleable.WindowAnimation_activityCloseEnterAnimation), anyBoolean()); } - class ChangeBuilder { - final TransitionInfo.Change mChange; - - ChangeBuilder(@WindowManager.TransitionType int mode) { - mChange = new TransitionInfo.Change(null /* token */, createMockSurface(true)); - 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<Pair<IBinder, Transitions.TransitionFinishCallback>> mFinishes = new ArrayList<>(); @@ -1739,12 +1751,6 @@ public class ShellTransitionTests extends ShellTestCase { .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); } - private static SurfaceControl createMockSurface(boolean valid) { - SurfaceControl sc = mock(SurfaceControl.class); - doReturn(valid).when(sc).isValid(); - return sc; - } - private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode, int activityType) { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionAnimationHelperTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionAnimationHelperTest.kt new file mode 100644 index 000000000000..bad14bbdb141 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionAnimationHelperTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2024 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.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration +import android.view.WindowManager +import android.window.TransitionInfo +import android.window.TransitionInfo.FLAG_TRANSLUCENT +import com.android.internal.R +import com.android.internal.policy.TransitionAnimation +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import org.junit.Test +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.verify + +class TransitionAnimationHelperTest : ShellTestCase() { + + @Mock + lateinit var transitionAnimation: TransitionAnimation + + @Test + fun loadAttributeAnimation_freeform_taskOpen_taskToBackChange_returnsMinimizeAnim() { + val openChange = ChangeBuilder(WindowManager.TRANSIT_OPEN) + .setTask(createTaskInfo(WindowConfiguration.WINDOWING_MODE_FREEFORM)) + .build() + val toBackChange = ChangeBuilder(WindowManager.TRANSIT_TO_BACK) + .setTask(createTaskInfo(WindowConfiguration.WINDOWING_MODE_FREEFORM)) + .build() + val info = TransitionInfoBuilder(WindowManager.TRANSIT_OPEN) + .addChange(openChange) + .addChange(toBackChange) + .build() + + loadAttributeAnimation(WindowManager.TRANSIT_OPEN, info, toBackChange) + + verify(transitionAnimation).loadDefaultAnimationAttr( + eq(R.styleable.WindowAnimation_activityCloseExitAnimation), anyBoolean()) + } + + @Test + fun loadAttributeAnimation_freeform_taskToFront_taskToFrontChange_returnsUnminimizeAnim() { + val toFrontChange = ChangeBuilder(WindowManager.TRANSIT_TO_FRONT) + .setTask(createTaskInfo(WindowConfiguration.WINDOWING_MODE_FREEFORM)) + .build() + val info = TransitionInfoBuilder(WindowManager.TRANSIT_TO_FRONT) + .addChange(toFrontChange) + .build() + + loadAttributeAnimation(WindowManager.TRANSIT_TO_FRONT, info, toFrontChange) + + verify(transitionAnimation).loadDefaultAnimationAttr( + eq(R.styleable.WindowAnimation_activityOpenEnterAnimation), + /* translucent= */ anyBoolean()) + } + + @Test + fun loadAttributeAnimation_fullscreen_taskOpen_returnsTaskOpenEnterAnim() { + val openChange = ChangeBuilder(WindowManager.TRANSIT_OPEN) + .setTask(createTaskInfo(WindowConfiguration.WINDOWING_MODE_FULLSCREEN)) + .build() + val info = TransitionInfoBuilder(WindowManager.TRANSIT_OPEN).addChange(openChange).build() + + loadAttributeAnimation(WindowManager.TRANSIT_OPEN, info, openChange) + + verify(transitionAnimation).loadDefaultAnimationAttr( + eq(R.styleable.WindowAnimation_taskOpenEnterAnimation), + /* translucent= */ anyBoolean()) + } + + @Test + fun loadAttributeAnimation_freeform_taskOpen_taskToBackChange_passesTranslucent() { + val openChange = ChangeBuilder(WindowManager.TRANSIT_OPEN) + .setTask(createTaskInfo(WindowConfiguration.WINDOWING_MODE_FREEFORM)) + .build() + val toBackChange = ChangeBuilder(WindowManager.TRANSIT_TO_BACK) + .setTask(createTaskInfo(WindowConfiguration.WINDOWING_MODE_FREEFORM)) + .setFlags(FLAG_TRANSLUCENT) + .build() + val info = TransitionInfoBuilder(WindowManager.TRANSIT_OPEN) + .addChange(openChange) + .addChange(toBackChange) + .build() + + loadAttributeAnimation(WindowManager.TRANSIT_OPEN, info, toBackChange) + + verify(transitionAnimation).loadDefaultAnimationAttr( + eq(R.styleable.WindowAnimation_activityCloseExitAnimation), + /* translucent= */ eq(true)) + } + + private fun loadAttributeAnimation( + @WindowManager.TransitionType type: Int, + info: TransitionInfo, + change: TransitionInfo.Change, + wallpaperTransit: Int = TransitionAnimation.WALLPAPER_TRANSITION_NONE, + isDreamTransition: Boolean = false, + ) { + TransitionAnimationHelper.loadAttributeAnimation( + type, info, change, wallpaperTransit, transitionAnimation, isDreamTransition) + } + + private fun createTaskInfo(windowingMode: Int): RunningTaskInfo { + val taskInfo = TestRunningTaskInfoBuilder() + .setWindowingMode(windowingMode) + .build() + return taskInfo + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java index b8939e6ff623..49ae182fef34 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java @@ -20,8 +20,10 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static org.mockito.Mockito.mock; +import android.annotation.Nullable; import android.app.ActivityManager; import android.content.ComponentName; +import android.os.IBinder; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.TransitionInfo; @@ -51,21 +53,24 @@ public class TransitionInfoBuilder { } public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, - @TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo, - ComponentName activityComponent) { + @TransitionInfo.ChangeFlags int flags, + @Nullable ActivityManager.RunningTaskInfo taskInfo, + @Nullable ComponentName activityComponent, @Nullable IBinder taskFragmentToken) { final TransitionInfo.Change change = new TransitionInfo.Change( taskInfo != null ? taskInfo.token : null, createMockSurface(true /* valid */)); change.setMode(mode); change.setFlags(flags); change.setTaskInfo(taskInfo); change.setActivityComponent(activityComponent); + change.setTaskFragmentToken(taskFragmentToken); return addChange(change); } /** Add a change to the TransitionInfo */ public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, @TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo) { - return addChange(mode, flags, taskInfo, null /* activityComponent */); + return addChange(mode, flags, taskInfo, null /* activityComponent */, + null /* taskFragmentToken */); } public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, @@ -76,13 +81,21 @@ public class TransitionInfoBuilder { /** Add a change to the TransitionInfo */ public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, ComponentName activityComponent) { - return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskinfo */, activityComponent); + return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskinfo */, activityComponent, + null /* taskFragmentToken */); } public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode) { return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskInfo */); } + /** Add a change with a TaskFragment token to the TransitionInfo */ + public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, + @Nullable IBinder taskFragmentToken) { + return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskInfo */, + null /* activityComponent */, taskFragmentToken); + } + public TransitionInfoBuilder addChange(TransitionInfo.Change change) { change.setDisplayId(DISPLAY_ID, DISPLAY_ID); mInfo.addChange(change); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/OWNERS new file mode 100644 index 000000000000..f5ba6143d496 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 1267635 +# includes OWNERS from parent directories
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldAnimationControllerTest.java index 8196c5ab08e4..8fe0c386b7fe 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldAnimationControllerTest.java @@ -35,7 +35,7 @@ import android.view.SurfaceControl.Transaction; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.TestShellExecutor; -import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java index acc0bce5cce9..cf2de91bad88 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java @@ -40,7 +40,7 @@ import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.TransitionInfoBuilder; import com.android.wm.shell.transition.Transitions; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationProtoUtils.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationProtoUtils.kt new file mode 100644 index 000000000000..def4b916a5f7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationProtoUtils.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.util + +import com.android.wm.shell.desktopmode.education.data.WindowingEducationProto + +/** + * Constructs a [WindowingEducationProto] object, populating its fields with the provided + * parameters. + * + * Any fields without corresponding parameters will retain their default values. + */ +fun createWindowingEducationProto( + educationViewedTimestampMillis: Long? = null, + featureUsedTimestampMillis: Long? = null, + appUsageStats: Map<String, Int>? = null, + appUsageStatsLastUpdateTimestampMillis: Long? = null +): WindowingEducationProto = + WindowingEducationProto.newBuilder() + .apply { + if (educationViewedTimestampMillis != null) { + setEducationViewedTimestampMillis(educationViewedTimestampMillis) + } + if (featureUsedTimestampMillis != null) { + setFeatureUsedTimestampMillis(featureUsedTimestampMillis) + } + setAppHandleEducation( + createAppHandleEducationProto(appUsageStats, appUsageStatsLastUpdateTimestampMillis)) + } + .build() + +/** + * Constructs a [WindowingEducationProto.AppHandleEducation] object, populating its fields with the + * provided parameters. + * + * Any fields without corresponding parameters will retain their default values. + */ +fun createAppHandleEducationProto( + appUsageStats: Map<String, Int>? = null, + appUsageStatsLastUpdateTimestampMillis: Long? = null +): WindowingEducationProto.AppHandleEducation = + WindowingEducationProto.AppHandleEducation.newBuilder() + .apply { + if (appUsageStats != null) putAllAppUsageStats(appUsageStats) + if (appUsageStatsLastUpdateTimestampMillis != null) { + setAppUsageStatsLastUpdateTimestampMillis(appUsageStatsLastUpdateTimestampMillis) + } + } + .build() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt new file mode 100644 index 000000000000..d141c2d771ce --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2024 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.windowdecor + +import android.app.ActivityManager +import android.app.WindowConfiguration +import android.content.ComponentName +import android.testing.AndroidTestingRunner +import android.view.Display +import android.view.InsetsState +import android.view.WindowInsetsController +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.google.common.truth.Truth +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class CaptionWindowDecorationTests : ShellTestCase() { + @Test + fun updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() { + val taskInfo = createTaskInfo() + taskInfo.configuration.windowConfiguration.windowingMode = + WindowConfiguration.WINDOWING_MODE_FREEFORM + taskInfo.taskDescription!!.topOpaqueSystemBarsAppearance = + WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND + val relayoutParams = WindowDecoration.RelayoutParams() + + CaptionWindowDecoration.updateRelayoutParams( + relayoutParams, + taskInfo, + true, + false, + InsetsState() + ) + + Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isTrue() + } + + @Test + fun updateRelayoutParams_freeformButOpaqueAppearance_disallowsInputFallthrough() { + val taskInfo = createTaskInfo() + taskInfo.configuration.windowConfiguration.windowingMode = + WindowConfiguration.WINDOWING_MODE_FREEFORM + taskInfo.taskDescription!!.topOpaqueSystemBarsAppearance = 0 + val relayoutParams = WindowDecoration.RelayoutParams() + + CaptionWindowDecoration.updateRelayoutParams( + relayoutParams, + taskInfo, + true, + false, + InsetsState() + ) + + Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isFalse() + } + + @Test + fun updateRelayoutParams_addOccludingCaptionElementCorrectly() { + val taskInfo = createTaskInfo() + val relayoutParams = WindowDecoration.RelayoutParams() + CaptionWindowDecoration.updateRelayoutParams( + relayoutParams, + taskInfo, + true, + false, + InsetsState() + ) + Truth.assertThat(relayoutParams.mOccludingCaptionElements.size).isEqualTo(2) + Truth.assertThat(relayoutParams.mOccludingCaptionElements[0].mAlignment).isEqualTo( + WindowDecoration.RelayoutParams.OccludingCaptionElement.Alignment.START) + Truth.assertThat(relayoutParams.mOccludingCaptionElements[1].mAlignment).isEqualTo( + WindowDecoration.RelayoutParams.OccludingCaptionElement.Alignment.END) + } + + private fun createTaskInfo(): ActivityManager.RunningTaskInfo { + val taskDescriptionBuilder = + ActivityManager.TaskDescription.Builder() + val taskInfo = TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setTaskDescriptionBuilder(taskDescriptionBuilder) + .setVisible(true) + .build() + taskInfo.realActivity = ComponentName( + "com.android.wm.shell.windowdecor", + "CaptionWindowDecorationTests" + ) + taskInfo.baseActivity = ComponentName( + "com.android.wm.shell.windowdecor", + "CaptionWindowDecorationTests" + ) + return taskInfo + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index ca1e3f173e24..ee2a41c322c9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -16,26 +16,33 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager.RunningTaskInfo -import android.app.WindowConfiguration import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.app.WindowConfiguration.WINDOWING_MODE_PINNED import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.app.WindowConfiguration.WindowingMode import android.content.ComponentName import android.content.Context +import android.content.Intent import android.content.pm.ActivityInfo import android.graphics.Rect import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.hardware.input.InputManager +import android.net.Uri import android.os.Handler +import android.os.SystemClock +import android.os.UserHandle +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.annotations.RequiresFlagsEnabled import android.platform.test.flag.junit.CheckFlagsRule import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner +import android.testing.TestableContext import android.testing.TestableLooper.RunWithLooper import android.util.SparseArray import android.view.Choreographer @@ -46,56 +53,86 @@ import android.view.InputMonitor import android.view.InsetsSource import android.view.InsetsState import android.view.KeyEvent +import android.view.MotionEvent +import android.view.Surface import android.view.SurfaceControl import android.view.SurfaceView import android.view.View -import android.view.WindowInsets.Type.navigationBars +import android.view.ViewRootImpl import android.view.WindowInsets.Type.statusBars +import android.widget.Toast +import android.window.WindowContainerTransaction +import android.window.WindowContainerTransaction.HierarchyOp import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.internal.jank.InteractionJankMonitor import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser +import com.android.wm.shell.apptoweb.AssistContentRequester +import com.android.wm.shell.common.DisplayChangeController import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayInsetsController import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler import com.android.wm.shell.desktopmode.DesktopTasksController +import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition +import com.android.wm.shell.desktopmode.DesktopTasksLimiter +import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository import com.android.wm.shell.freeform.FreeformTaskTransitionStarter -import com.android.wm.shell.shared.DesktopModeStatus -import com.android.wm.shell.sysui.KeyguardChangeListener +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource +import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeKeyguardChangeListener import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener +import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder +import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier import java.util.Optional +import java.util.function.Consumer import java.util.function.Supplier +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentCaptor.forClass import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.anyInt import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.times -import org.mockito.Mockito.verify +import org.mockito.kotlin.verify import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argThat import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doNothing import org.mockito.kotlin.eq +import org.mockito.kotlin.mock import org.mockito.kotlin.spy import org.mockito.kotlin.whenever import org.mockito.quality.Strictness + /** * Tests of [DesktopModeWindowDecorViewModel] * Usage: atest WMShellUnitTests:DesktopModeWindowDecorViewModelTests @@ -118,6 +155,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockMainChoreographer: Choreographer @Mock private lateinit var mockTaskOrganizer: ShellTaskOrganizer @Mock private lateinit var mockDisplayController: DisplayController + @Mock private lateinit var mockSplitScreenController: SplitScreenController @Mock private lateinit var mockDisplayLayout: DisplayLayout @Mock private lateinit var displayInsetsController: DisplayInsetsController @Mock private lateinit var mockSyncQueue: SyncTransactionQueue @@ -128,28 +166,63 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { DesktopModeWindowDecorViewModel.InputMonitorFactory @Mock private lateinit var mockShellController: ShellController @Mock private lateinit var mockShellExecutor: ShellExecutor + @Mock private lateinit var mockAppHeaderViewHolderFactory: AppHeaderViewHolder.Factory @Mock private lateinit var mockRootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock private lateinit var mockShellCommandHandler: ShellCommandHandler @Mock private lateinit var mockWindowManager: IWindowManager + @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor + @Mock private lateinit var mockGenericLinksParser: AppToWebGenericLinksParser + @Mock private lateinit var mockUserHandle: UserHandle + @Mock private lateinit var mockAssistContentRequester: AssistContentRequester + @Mock private lateinit var mockToast: Toast + private val bgExecutor = TestShellExecutor() + @Mock private lateinit var mockMultiInstanceHelper: MultiInstanceHelper + @Mock private lateinit var mockTasksLimiter: DesktopTasksLimiter + @Mock private lateinit var mockFreeformTaskTransitionStarter: FreeformTaskTransitionStarter + @Mock private lateinit var mockActivityOrientationChangeHandler: + DesktopActivityOrientationChangeHandler + @Mock private lateinit var mockInputManager: InputManager + @Mock private lateinit var mockTaskPositionerFactory: + DesktopModeWindowDecorViewModel.TaskPositionerFactory + @Mock private lateinit var mockTaskPositioner: TaskPositioner + @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository + @Mock private lateinit var mockWindowDecorViewHostSupplier: WindowDecorViewHostSupplier<*> + private lateinit var spyContext: TestableContext private val transactionFactory = Supplier<SurfaceControl.Transaction> { SurfaceControl.Transaction() } private val windowDecorByTaskIdSpy = spy(SparseArray<DesktopModeWindowDecoration>()) + private lateinit var mockitoSession: StaticMockitoSession private lateinit var shellInit: ShellInit private lateinit var desktopModeOnInsetsChangedListener: DesktopModeOnInsetsChangedListener + private lateinit var displayChangingListener: DisplayChangeController.OnDisplayChangingListener + private lateinit var desktopModeOnKeyguardChangedListener: DesktopModeKeyguardChangeListener private lateinit var desktopModeWindowDecorViewModel: DesktopModeWindowDecorViewModel @Before fun setUp() { + mockitoSession = + mockitoSession() + .strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java) + .spyStatic(DragPositioningCallbackUtility::class.java) + .spyStatic(Toast::class.java) + .startMocking() + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(Mockito.any()) } + + spyContext = spy(mContext) + doNothing().`when`(spyContext).startActivity(any()) shellInit = ShellInit(mockShellExecutor) windowDecorByTaskIdSpy.clear() + spyContext.addMockSystemService(InputManager::class.java, mockInputManager) desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel( - mContext, + spyContext, mockShellExecutor, mockMainHandler, mockMainChoreographer, + bgExecutor, shellInit, mockShellCommandHandler, mockWindowManager, @@ -160,16 +233,41 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockSyncQueue, mockTransitions, Optional.of(mockDesktopTasksController), + mockGenericLinksParser, + mockAssistContentRequester, + mockMultiInstanceHelper, + mockWindowDecorViewHostSupplier, mockDesktopModeWindowDecorFactory, mockInputMonitorFactory, transactionFactory, + mockAppHeaderViewHolderFactory, mockRootTaskDisplayAreaOrganizer, - windowDecorByTaskIdSpy + windowDecorByTaskIdSpy, + mockInteractionJankMonitor, + Optional.of(mockTasksLimiter), + mockCaptionHandleRepository, + Optional.of(mockActivityOrientationChangeHandler), + mockTaskPositionerFactory ) - + desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) whenever(mockDisplayLayout.stableInsets()).thenReturn(STABLE_INSETS) whenever(mockInputMonitorFactory.create(any(), any())).thenReturn(mockInputMonitor) + whenever( + mockTaskPositionerFactory.create( + any(), + any(), + any(), + any(), + any(), + any(), + any(), + any() + ) + ) + .thenReturn(mockTaskPositioner) + + doReturn(mockToast).`when` { Toast.makeText(any(), anyInt(), anyInt()) } // InputChannel cannot be mocked because it passes to InputEventReceiver. val inputChannels = InputChannel.openInputChannelPair(TAG) @@ -178,10 +276,25 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { shellInit.init() - val listenerCaptor = + val displayChangingListenerCaptor = + argumentCaptor<DisplayChangeController.OnDisplayChangingListener>() + verify(mockDisplayController) + .addDisplayChangingController(displayChangingListenerCaptor.capture()) + displayChangingListener = displayChangingListenerCaptor.firstValue + val insetsChangedCaptor = argumentCaptor<DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener>() - verify(displayInsetsController).addInsetsChangedListener(anyInt(), listenerCaptor.capture()) - desktopModeOnInsetsChangedListener = listenerCaptor.firstValue + verify(displayInsetsController) + .addGlobalInsetsChangedListener(insetsChangedCaptor.capture()) + desktopModeOnInsetsChangedListener = insetsChangedCaptor.firstValue + val keyguardChangedCaptor = + argumentCaptor<DesktopModeKeyguardChangeListener>() + verify(mockShellController).addKeyguardChangeListener(keyguardChangedCaptor.capture()) + desktopModeOnKeyguardChangedListener = keyguardChangedCaptor.firstValue + } + + @After + fun tearDown() { + mockitoSession.finishMocking() } @Test @@ -191,22 +304,13 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { val decoration = setUpMockDecorationForTask(task) onTaskOpening(task, taskSurface) + assertTrue(windowDecorByTaskIdSpy.contains(task.taskId)) task.setWindowingMode(WINDOWING_MODE_UNDEFINED) task.setActivityType(ACTIVITY_TYPE_UNDEFINED) onTaskChanging(task, taskSurface) - verify(mockDesktopModeWindowDecorFactory).create( - mContext, - mockDisplayController, - mockTaskOrganizer, - task, - taskSurface, - mockMainHandler, - mockMainChoreographer, - mockSyncQueue, - mockRootTaskDisplayAreaOrganizer - ) + assertFalse(windowDecorByTaskIdSpy.contains(task.taskId)) verify(decoration).close() } @@ -220,47 +324,26 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { setUpMockDecorationForTask(task) onTaskChanging(task, taskSurface) - verify(mockDesktopModeWindowDecorFactory, never()).create( - mContext, - mockDisplayController, - mockTaskOrganizer, - task, - taskSurface, - mockMainHandler, - mockMainChoreographer, - mockSyncQueue, - mockRootTaskDisplayAreaOrganizer - ) + assertFalse(windowDecorByTaskIdSpy.contains(task.taskId)) task.setWindowingMode(WINDOWING_MODE_FREEFORM) task.setActivityType(ACTIVITY_TYPE_STANDARD) onTaskChanging(task, taskSurface) - verify(mockDesktopModeWindowDecorFactory, times(1)).create( - mContext, - mockDisplayController, - mockTaskOrganizer, - task, - taskSurface, - mockMainHandler, - mockMainChoreographer, - mockSyncQueue, - mockRootTaskDisplayAreaOrganizer - ) + assertTrue(windowDecorByTaskIdSpy.contains(task.taskId)) } @Test + @DisableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX) fun testCreateAndDisposeEventReceiver() { - val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) - setUpMockDecorationForTask(task) - - onTaskOpening(task) - desktopModeWindowDecorViewModel.destroyWindowDecoration(task) + val decor = createOpenTaskDecoration(windowingMode = WINDOWING_MODE_FREEFORM) + desktopModeWindowDecorViewModel.destroyWindowDecoration(decor.mTaskInfo) verify(mockInputMonitorFactory).create(any(), any()) verify(mockInputMonitor).dispose() } @Test + @DisableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX) fun testEventReceiversOnMultipleDisplays() { val secondaryDisplay = createVirtualDisplay() ?: return val secondaryDisplayId = secondaryDisplay.display.displayId @@ -306,11 +389,10 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { whenever(view.id).thenReturn(R.id.back_button) val inputManager = mock(InputManager::class.java) - mContext.addMockSystemService(InputManager::class.java, inputManager) + spyContext.addMockSystemService(InputManager::class.java, inputManager) - val freeformTaskTransitionStarter = mock(FreeformTaskTransitionStarter::class.java) desktopModeWindowDecorViewModel - .setFreeformTaskTransitionStarter(freeformTaskTransitionStarter) + .setFreeformTaskTransitionStarter(mockFreeformTaskTransitionStarter) onClickListener.onClick(view) @@ -322,40 +404,94 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test - fun testCaptionIsNotCreatedWhenKeyguardIsVisible() { - val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true) - val keyguardListenerCaptor = argumentCaptor<KeyguardChangeListener>() - verify(mockShellController).addKeyguardChangeListener(keyguardListenerCaptor.capture()) + fun testCloseButtonInFreeform_closeWindow() { + val onClickListenerCaptor = forClass(View.OnClickListener::class.java) + as ArgumentCaptor<View.OnClickListener> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + onCaptionButtonClickListener = onClickListenerCaptor + ) - keyguardListenerCaptor.firstValue.onKeyguardVisibilityChanged( - true /* visible */, - true /* occluded */, - false /* animatingDismiss */ + val view = mock(View::class.java) + whenever(view.id).thenReturn(R.id.close_window) + + desktopModeWindowDecorViewModel + .setFreeformTaskTransitionStarter(mockFreeformTaskTransitionStarter) + + onClickListenerCaptor.value.onClick(view) + + val transactionCaptor = argumentCaptor<WindowContainerTransaction>() + verify(mockFreeformTaskTransitionStarter).startRemoveTransition(transactionCaptor.capture()) + val wct = transactionCaptor.firstValue + + assertEquals(1, wct.getHierarchyOps().size) + assertEquals(HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK, + wct.getHierarchyOps().get(0).getType()) + assertEquals(decor.mTaskInfo.token.asBinder(), wct.getHierarchyOps().get(0).getContainer()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MINIMIZE_BUTTON) + fun testMinimizeButtonInFreefrom_minimizeWindow() { + val onClickListenerCaptor = forClass(View.OnClickListener::class.java) + as ArgumentCaptor<View.OnClickListener> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + onCaptionButtonClickListener = onClickListenerCaptor ) - onTaskOpening(task) - task.setWindowingMode(WINDOWING_MODE_UNDEFINED) - task.setWindowingMode(ACTIVITY_TYPE_UNDEFINED) - onTaskChanging(task) + val view = mock(View::class.java) + whenever(view.id).thenReturn(R.id.minimize_window) + + desktopModeWindowDecorViewModel + .setFreeformTaskTransitionStarter(mockFreeformTaskTransitionStarter) + + onClickListenerCaptor.value.onClick(view) - verify(mockDesktopModeWindowDecorFactory, never()) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + val transactionCaptor = argumentCaptor<WindowContainerTransaction>() + verify(mockFreeformTaskTransitionStarter) + .startMinimizedModeTransition(transactionCaptor.capture()) + val wct = transactionCaptor.firstValue + + verify(mockTasksLimiter).addPendingMinimizeChange( + anyOrNull(), eq(DEFAULT_DISPLAY), eq(decor.mTaskInfo.taskId)) + + assertEquals(1, wct.getHierarchyOps().size) + assertEquals(HierarchyOp.HIERARCHY_OP_TYPE_REORDER, wct.getHierarchyOps().get(0).getType()) + assertFalse(wct.getHierarchyOps().get(0).getToTop()) + assertEquals(decor.mTaskInfo.token.asBinder(), wct.getHierarchyOps().get(0).getContainer()) } @Test - fun testDecorationIsNotCreatedForTopTranslucentActivities() { - setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun testDecorationIsCreatedForTopTranslucentActivitiesWithStyleFloating() { val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true).apply { isTopActivityTransparent = true + isTopActivityStyleFloating = true numActivities = 1 } + doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) } + setUpMockDecorationsForTasks(task) + onTaskOpening(task) + assertTrue(windowDecorByTaskIdSpy.contains(task.taskId)) + } - verify(mockDesktopModeWindowDecorFactory, never()) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun testDecorationIsNotCreatedForTopTranslucentActivitiesWithoutStyleFloating() { + val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true).apply { + isTopActivityTransparent = true + isTopActivityStyleFloating = false + numActivities = 1 + } + onTaskOpening(task) + + assertFalse(windowDecorByTaskIdSpy.contains(task.taskId)) } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) fun testDecorationIsNotCreatedForSystemUIActivities() { val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true) @@ -367,155 +503,676 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskOpening(task) - verify(mockDesktopModeWindowDecorFactory, never()) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + assertFalse(windowDecorByTaskIdSpy.contains(task.taskId)) } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING) - fun testRelayoutRunsWhenStatusBarsInsetsSourceVisibilityChanges() { - val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true) - val decoration = setUpMockDecorationForTask(task) - - onTaskOpening(task) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING) + fun testInsetsStateChanged_notifiesAllDecorsInDisplay() { + val task1 = createTask(windowingMode = WINDOWING_MODE_FREEFORM, displayId = 1) + val decoration1 = setUpMockDecorationForTask(task1) + onTaskOpening(task1) + val task2 = createTask(windowingMode = WINDOWING_MODE_FREEFORM, displayId = 2) + val decoration2 = setUpMockDecorationForTask(task2) + onTaskOpening(task2) + val task3 = createTask(windowingMode = WINDOWING_MODE_FREEFORM, displayId = 2) + val decoration3 = setUpMockDecorationForTask(task3) + onTaskOpening(task3) // Add status bar insets source - val insetsState = InsetsState() - val statusBarInsetsSourceId = 0 - val statusBarInsetsSource = InsetsSource(statusBarInsetsSourceId, statusBars()) - statusBarInsetsSource.isVisible = false - insetsState.addSource(statusBarInsetsSource) + val insetsState = InsetsState().apply { + addSource(InsetsSource(0 /* id */, statusBars()).apply { + isVisible = false + }) + } + desktopModeOnInsetsChangedListener.insetsChanged(2 /* displayId */, insetsState) - desktopModeOnInsetsChangedListener.insetsChanged(insetsState) + verify(decoration1, never()).onInsetsStateChanged(insetsState) + verify(decoration2).onInsetsStateChanged(insetsState) + verify(decoration3).onInsetsStateChanged(insetsState) + } - // Verify relayout occurs when status bar inset visibility changes - verify(decoration, times(1)).relayout(task) + @Test + fun testKeyguardState_notifiesAllDecors() { + val decoration1 = createOpenTaskDecoration(windowingMode = WINDOWING_MODE_FREEFORM) + val decoration2 = createOpenTaskDecoration(windowingMode = WINDOWING_MODE_FREEFORM) + val decoration3 = createOpenTaskDecoration(windowingMode = WINDOWING_MODE_FREEFORM) + + desktopModeOnKeyguardChangedListener + .onKeyguardVisibilityChanged(true /* visible */, true /* occluded */, + false /* animatingDismiss */) + + verify(decoration1).onKeyguardStateChanged(true /* visible */, true /* occluded */) + verify(decoration2).onKeyguardStateChanged(true /* visible */, true /* occluded */) + verify(decoration3).onKeyguardStateChanged(true /* visible */, true /* occluded */) } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING) - fun testRelayoutDoesNotRunWhenNonStatusBarsInsetsSourceVisibilityChanges() { - val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true) + fun testDestroyWindowDecoration_closesBeforeCleanup() { + val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) val decoration = setUpMockDecorationForTask(task) + val inOrder = Mockito.inOrder(decoration, windowDecorByTaskIdSpy) + + onTaskOpening(task) + desktopModeWindowDecorViewModel.destroyWindowDecoration(task) + + inOrder.verify(decoration).close() + inOrder.verify(windowDecorByTaskIdSpy).remove(task.taskId) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + fun testWindowDecor_desktopModeUnsupportedOnDevice_decorNotCreated() { + // Simulate default enforce device restrictions system property + whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) + + val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true) + // Simulate device that doesn't support desktop mode + doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } onTaskOpening(task) + assertFalse(windowDecorByTaskIdSpy.contains(task.taskId)) + } - // Add navigation bar insets source - val insetsState = InsetsState() - val navigationBarInsetsSourceId = 1 - val navigationBarInsetsSource = InsetsSource(navigationBarInsetsSourceId, navigationBars()) - navigationBarInsetsSource.isVisible = false - insetsState.addSource(navigationBarInsetsSource) + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + fun testWindowDecor_desktopModeUnsupportedOnDevice_deviceRestrictionsOverridden_decorCreated() { + // Simulate enforce device restrictions system property overridden to false + whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(false) + // Simulate device that doesn't support desktop mode + doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - desktopModeOnInsetsChangedListener.insetsChanged(insetsState) + val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true) + setUpMockDecorationsForTasks(task) - // Verify relayout does not occur when non-status bar inset changes visibility - verify(decoration, never()).relayout(task) + onTaskOpening(task) + assertTrue(windowDecorByTaskIdSpy.contains(task.taskId)) } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING) - fun testRelayoutDoesNotRunWhenNonStatusBarsInsetSourceVisibilityDoesNotChange() { - val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true) - val decoration = setUpMockDecorationForTask(task) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + fun testWindowDecor_deviceSupportsDesktopMode_decorCreated() { + // Simulate default enforce device restrictions system property + whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) + + val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true) + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + setUpMockDecorationsForTasks(task) onTaskOpening(task) + assertTrue(windowDecorByTaskIdSpy.contains(task.taskId)) + } - // Add status bar insets source - val insetsState = InsetsState() - val statusBarInsetsSourceId = 0 - val statusBarInsetsSource = InsetsSource(statusBarInsetsSourceId, statusBars()) - statusBarInsetsSource.isVisible = false - insetsState.addSource(statusBarInsetsSource) + @Test + fun testOnDecorMaximizedOrRestored_togglesTaskSize() { + val maxOrRestoreListenerCaptor = forClass(Function0::class.java) + as ArgumentCaptor<Function0<Unit>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + onMaxOrRestoreListenerCaptor = maxOrRestoreListenerCaptor + ) - desktopModeOnInsetsChangedListener.insetsChanged(insetsState) - desktopModeOnInsetsChangedListener.insetsChanged(insetsState) + maxOrRestoreListenerCaptor.value.invoke() - // Verify relayout runs only once when status bar inset visibility changes. - verify(decoration, times(1)).relayout(task) + verify(mockDesktopTasksController).toggleDesktopTaskSize(decor.mTaskInfo) } @Test - fun testDestroyWindowDecoration_closesBeforeCleanup() { - val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) - val decoration = setUpMockDecorationForTask(task) - val inOrder = Mockito.inOrder(decoration, windowDecorByTaskIdSpy) + fun testOnDecorMaximizedOrRestored_closesMenus() { + val maxOrRestoreListenerCaptor = forClass(Function0::class.java) + as ArgumentCaptor<Function0<Unit>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + onMaxOrRestoreListenerCaptor = maxOrRestoreListenerCaptor + ) + + maxOrRestoreListenerCaptor.value.invoke() + + verify(decor).closeHandleMenu() + verify(decor).closeMaximizeMenu() + } + + @Test + fun testOnDecorSnappedLeft_snapResizes() { + val taskSurfaceCaptor = argumentCaptor<SurfaceControl>() + val onLeftSnapClickListenerCaptor = forClass(Function0::class.java) + as ArgumentCaptor<Function0<Unit>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + onLeftSnapClickListenerCaptor = onLeftSnapClickListenerCaptor + ) + + val currentBounds = decor.mTaskInfo.configuration.windowConfiguration.bounds + onLeftSnapClickListenerCaptor.value.invoke() + + verify(mockDesktopTasksController).snapToHalfScreen( + eq(decor.mTaskInfo), + taskSurfaceCaptor.capture(), + eq(currentBounds), + eq(SnapPosition.LEFT) + ) + assertEquals(taskSurfaceCaptor.firstValue, decor.mTaskSurface) + } + + @Test + fun testOnDecorSnappedLeft_closeMenus() { + val onLeftSnapClickListenerCaptor = forClass(Function0::class.java) + as ArgumentCaptor<Function0<Unit>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + onLeftSnapClickListenerCaptor = onLeftSnapClickListenerCaptor + ) + + onLeftSnapClickListenerCaptor.value.invoke() + + verify(decor).closeHandleMenu() + verify(decor).closeMaximizeMenu() + } + + @Test + @DisableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING) + fun testOnSnapResizeLeft_nonResizable_decorSnappedLeft() { + val taskSurfaceCaptor = argumentCaptor<SurfaceControl>() + val onLeftSnapClickListenerCaptor = forClass(Function0::class.java) + as ArgumentCaptor<Function0<Unit>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + onLeftSnapClickListenerCaptor = onLeftSnapClickListenerCaptor + ).also { it.mTaskInfo.isResizeable = false } + + val currentBounds = decor.mTaskInfo.configuration.windowConfiguration.bounds + onLeftSnapClickListenerCaptor.value.invoke() + + verify(mockDesktopTasksController).snapToHalfScreen( + eq(decor.mTaskInfo), + taskSurfaceCaptor.capture(), + eq(currentBounds), + eq(SnapPosition.LEFT) + ) + assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue) + } + + @Test + @EnableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING) + fun testOnSnapResizeLeft_nonResizable_decorNotSnapped() { + val onLeftSnapClickListenerCaptor = forClass(Function0::class.java) + as ArgumentCaptor<Function0<Unit>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + onLeftSnapClickListenerCaptor = onLeftSnapClickListenerCaptor + ).also { it.mTaskInfo.isResizeable = false } + + val currentBounds = decor.mTaskInfo.configuration.windowConfiguration.bounds + onLeftSnapClickListenerCaptor.value.invoke() + + verify(mockDesktopTasksController, never()) + .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT)) + verify(mockToast).show() + } + + @Test + fun testOnDecorSnappedRight_snapResizes() { + val taskSurfaceCaptor = argumentCaptor<SurfaceControl>() + val onRightSnapClickListenerCaptor = forClass(Function0::class.java) + as ArgumentCaptor<Function0<Unit>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + onRightSnapClickListenerCaptor = onRightSnapClickListenerCaptor + ) + + val currentBounds = decor.mTaskInfo.configuration.windowConfiguration.bounds + onRightSnapClickListenerCaptor.value.invoke() + + verify(mockDesktopTasksController).snapToHalfScreen( + eq(decor.mTaskInfo), + taskSurfaceCaptor.capture(), + eq(currentBounds), + eq(SnapPosition.RIGHT) + ) + assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue) + } + + @Test + fun testOnDecorSnappedRight_closeMenus() { + val onRightSnapClickListenerCaptor = forClass(Function0::class.java) + as ArgumentCaptor<Function0<Unit>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + onRightSnapClickListenerCaptor = onRightSnapClickListenerCaptor + ) + + onRightSnapClickListenerCaptor.value.invoke() + + verify(decor).closeHandleMenu() + verify(decor).closeMaximizeMenu() + } + + @Test + @DisableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING) + fun testOnSnapResizeRight_nonResizable_decorSnappedRight() { + val taskSurfaceCaptor = argumentCaptor<SurfaceControl>() + val onRightSnapClickListenerCaptor = forClass(Function0::class.java) + as ArgumentCaptor<Function0<Unit>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + onRightSnapClickListenerCaptor = onRightSnapClickListenerCaptor + ).also { it.mTaskInfo.isResizeable = false } + + val currentBounds = decor.mTaskInfo.configuration.windowConfiguration.bounds + onRightSnapClickListenerCaptor.value.invoke() + + verify(mockDesktopTasksController).snapToHalfScreen( + eq(decor.mTaskInfo), + taskSurfaceCaptor.capture(), + eq(currentBounds), + eq(SnapPosition.RIGHT) + ) + assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue) + } + + @Test + @EnableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING) + fun testOnSnapResizeRight_nonResizable_decorNotSnapped() { + val onRightSnapClickListenerCaptor = forClass(Function0::class.java) + as ArgumentCaptor<Function0<Unit>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + onRightSnapClickListenerCaptor = onRightSnapClickListenerCaptor + ).also { it.mTaskInfo.isResizeable = false } + + val currentBounds = decor.mTaskInfo.configuration.windowConfiguration.bounds + onRightSnapClickListenerCaptor.value.invoke() + + verify(mockDesktopTasksController, never()) + .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT)) + verify(mockToast).show() + } + + @Test + fun testDecor_onClickToDesktop_movesToDesktopWithSource() { + val toDesktopListenerCaptor = forClass(Consumer::class.java) + as ArgumentCaptor<Consumer<DesktopModeTransitionSource>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FULLSCREEN, + onToDesktopClickListenerCaptor = toDesktopListenerCaptor + ) + + toDesktopListenerCaptor.value.accept(DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON) + + verify(mockDesktopTasksController).moveTaskToDesktop( + eq(decor.mTaskInfo.taskId), + any(), + eq(DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON) + ) + } + + @Test + fun testDecor_onClickToDesktop_addsCaptionInsets() { + val toDesktopListenerCaptor = forClass(Consumer::class.java) + as ArgumentCaptor<Consumer<DesktopModeTransitionSource>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FULLSCREEN, + onToDesktopClickListenerCaptor = toDesktopListenerCaptor + ) + + toDesktopListenerCaptor.value.accept(DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON) + + verify(decor).addCaptionInset(any()) + } + + @Test + fun testDecor_onClickToDesktop_closesHandleMenu() { + val toDesktopListenerCaptor = forClass(Consumer::class.java) + as ArgumentCaptor<Consumer<DesktopModeTransitionSource>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FULLSCREEN, + onToDesktopClickListenerCaptor = toDesktopListenerCaptor + ) + + toDesktopListenerCaptor.value.accept(DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON) + + verify(decor).closeHandleMenu() + } + + @Test + fun testDecor_onClickToFullscreen_closesHandleMenu() { + val toFullscreenListenerCaptor = forClass(Function0::class.java) + as ArgumentCaptor<Function0<Unit>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + onToFullscreenClickListenerCaptor = toFullscreenListenerCaptor + ) + + toFullscreenListenerCaptor.value.invoke() + + verify(decor).closeHandleMenu() + } + + @Test + fun testDecor_onClickToFullscreen_isFreeform_movesToFullscreen() { + val toFullscreenListenerCaptor = forClass(Function0::class.java) + as ArgumentCaptor<Function0<Unit>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + onToFullscreenClickListenerCaptor = toFullscreenListenerCaptor + ) + + toFullscreenListenerCaptor.value.invoke() + + verify(mockDesktopTasksController).moveToFullscreen( + decor.mTaskInfo.taskId, + DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON + ) + } + + @Test + fun testDecor_onClickToFullscreen_isSplit_movesToFullscreen() { + val toFullscreenListenerCaptor = forClass(Function0::class.java) + as ArgumentCaptor<Function0<Unit>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_MULTI_WINDOW, + onToFullscreenClickListenerCaptor = toFullscreenListenerCaptor + ) + + toFullscreenListenerCaptor.value.invoke() + + verify(mockSplitScreenController).moveTaskToFullscreen( + decor.mTaskInfo.taskId, + SplitScreenController.EXIT_REASON_DESKTOP_MODE + ) + } + + @Test + fun testDecor_onClickToSplitScreen_closesHandleMenu() { + val toSplitScreenListenerCaptor = forClass(Function0::class.java) + as ArgumentCaptor<Function0<Unit>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_MULTI_WINDOW, + onToSplitScreenClickListenerCaptor = toSplitScreenListenerCaptor + ) + + toSplitScreenListenerCaptor.value.invoke() + + verify(decor).closeHandleMenu() + } + + @Test + fun testDecor_onClickToSplitScreen_requestsSplit() { + val toSplitScreenListenerCaptor = forClass(Function0::class.java) + as ArgumentCaptor<Function0<Unit>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_MULTI_WINDOW, + onToSplitScreenClickListenerCaptor = toSplitScreenListenerCaptor + ) + + toSplitScreenListenerCaptor.value.invoke() + + verify(mockDesktopTasksController).requestSplit(decor.mTaskInfo, leftOrTop = false) + } + + @Test + fun testDecor_onClickToSplitScreen_disposesStatusBarInputLayer() { + val toSplitScreenListenerCaptor = forClass(Function0::class.java) + as ArgumentCaptor<Function0<Unit>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_MULTI_WINDOW, + onToSplitScreenClickListenerCaptor = toSplitScreenListenerCaptor + ) + + toSplitScreenListenerCaptor.value.invoke() + + verify(decor).disposeStatusBarInputLayer() + } + + @Test + fun testDecor_onClickToOpenBrowser_closeMenus() { + val openInBrowserListenerCaptor = forClass(Consumer::class.java) + as ArgumentCaptor<Consumer<Uri>> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FULLSCREEN, + onOpenInBrowserClickListener = openInBrowserListenerCaptor + ) + + openInBrowserListenerCaptor.value.accept(Uri.EMPTY) + + verify(decor).closeHandleMenu() + verify(decor).closeMaximizeMenu() + } + + @Test + fun testDecor_onClickToOpenBrowser_opensBrowser() { + doNothing().whenever(spyContext).startActivity(any()) + val uri = Uri.parse("https://www.google.com") + val openInBrowserListenerCaptor = forClass(Consumer::class.java) + as ArgumentCaptor<Consumer<Uri>> + createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FULLSCREEN, + onOpenInBrowserClickListener = openInBrowserListenerCaptor + ) + + openInBrowserListenerCaptor.value.accept(uri) + + verify(spyContext).startActivityAsUser(argThat { intent -> + intent.data == uri + && ((intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK) != 0) + && intent.categories.contains(Intent.CATEGORY_LAUNCHER) + && intent.action == Intent.ACTION_MAIN + }, eq(mockUserHandle)) + } + + @Test + fun testOnDisplayRotation_tasksOutOfValidArea_taskBoundsUpdated() { + val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM) + val secondTask = + createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM) + val thirdTask = + createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM) + + doReturn(true).`when` { + DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(any(), any()) + } + setUpMockDecorationsForTasks(task, secondTask, thirdTask) onTaskOpening(task) - desktopModeWindowDecorViewModel.destroyWindowDecoration(task) + onTaskOpening(secondTask) + onTaskOpening(thirdTask) - inOrder.verify(decoration).close() - inOrder.verify(windowDecorByTaskIdSpy).remove(task.taskId) + val wct = mock<WindowContainerTransaction>() + + displayChangingListener.onDisplayChange( + task.displayId, Surface.ROTATION_0, Surface.ROTATION_90, null, wct + ) + + verify(wct).setBounds(eq(task.token), any()) + verify(wct).setBounds(eq(secondTask.token), any()) + verify(wct).setBounds(eq(thirdTask.token), any()) } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - fun testWindowDecor_desktopModeUnsupportedOnDevice_decorNotCreated() { - val mockitoSession: StaticMockitoSession = mockitoSession() - .strictness(Strictness.LENIENT) - .spyStatic(DesktopModeStatus::class.java) - .startMocking() - try { - // Simulate default enforce device restrictions system property - whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) - - val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true) - // Simulate device that doesn't support desktop mode - doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - - onTaskOpening(task) - verify(mockDesktopModeWindowDecorFactory, never()) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) - } finally { - mockitoSession.finishMocking() + fun testOnDisplayRotation_taskInValidArea_taskBoundsNotUpdated() { + val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM) + val secondTask = + createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM) + val thirdTask = + createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM) + + doReturn(false).`when` { + DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(any(), any()) } + setUpMockDecorationsForTasks(task, secondTask, thirdTask) + + onTaskOpening(task) + onTaskOpening(secondTask) + onTaskOpening(thirdTask) + + val wct = mock<WindowContainerTransaction>() + displayChangingListener.onDisplayChange( + task.displayId, Surface.ROTATION_0, Surface.ROTATION_90, null, wct + ) + + verify(wct, never()).setBounds(eq(task.token), any()) + verify(wct, never()).setBounds(eq(secondTask.token), any()) + verify(wct, never()).setBounds(eq(thirdTask.token), any()) } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - fun testWindowDecor_desktopModeUnsupportedOnDevice_deviceRestrictionsOverridden_decorCreated() { - val mockitoSession: StaticMockitoSession = mockitoSession() - .strictness(Strictness.LENIENT) - .spyStatic(DesktopModeStatus::class.java) - .startMocking() - try { - // Simulate enforce device restrictions system property overridden to false - whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(false) - // Simulate device that doesn't support desktop mode - doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - - val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true) - setUpMockDecorationsForTasks(task) - - onTaskOpening(task) - verify(mockDesktopModeWindowDecorFactory) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) - } finally { - mockitoSession.finishMocking() + fun testOnDisplayRotation_sameOrientationRotation_taskBoundsNotUpdated() { + val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM) + val secondTask = + createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM) + val thirdTask = + createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM) + + setUpMockDecorationsForTasks(task, secondTask, thirdTask) + + onTaskOpening(task) + onTaskOpening(secondTask) + onTaskOpening(thirdTask) + + val wct = mock<WindowContainerTransaction>() + displayChangingListener.onDisplayChange( + task.displayId, Surface.ROTATION_0, Surface.ROTATION_180, null, wct + ) + + verify(wct, never()).setBounds(eq(task.token), any()) + verify(wct, never()).setBounds(eq(secondTask.token), any()) + verify(wct, never()).setBounds(eq(thirdTask.token), any()) + } + + @Test + fun testOnDisplayRotation_differentDisplayId_taskBoundsNotUpdated() { + val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM) + val secondTask = createTask(displayId = -2, windowingMode = WINDOWING_MODE_FREEFORM) + val thirdTask = createTask(displayId = -3, windowingMode = WINDOWING_MODE_FREEFORM) + + doReturn(true).`when` { + DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(any(), any()) } + setUpMockDecorationsForTasks(task, secondTask, thirdTask) + + onTaskOpening(task) + onTaskOpening(secondTask) + onTaskOpening(thirdTask) + + val wct = mock<WindowContainerTransaction>() + displayChangingListener.onDisplayChange( + task.displayId, Surface.ROTATION_0, Surface.ROTATION_90, null, wct + ) + + verify(wct).setBounds(eq(task.token), any()) + verify(wct, never()).setBounds(eq(secondTask.token), any()) + verify(wct, never()).setBounds(eq(thirdTask.token), any()) } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - fun testWindowDecor_deviceSupportsDesktopMode_decorCreated() { - val mockitoSession: StaticMockitoSession = mockitoSession() - .strictness(Strictness.LENIENT) - .spyStatic(DesktopModeStatus::class.java) - .startMocking() - try { - // Simulate default enforce device restrictions system property - whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) - - val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true) - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - setUpMockDecorationsForTasks(task) - - onTaskOpening(task) - verify(mockDesktopModeWindowDecorFactory) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) - } finally { - mockitoSession.finishMocking() + fun testOnDisplayRotation_nonFreeformTask_taskBoundsNotUpdated() { + val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM) + val secondTask = createTask(displayId = -2, windowingMode = WINDOWING_MODE_FULLSCREEN) + val thirdTask = createTask(displayId = -3, windowingMode = WINDOWING_MODE_PINNED) + + doReturn(true).`when` { + DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(any(), any()) } + setUpMockDecorationsForTasks(task, secondTask, thirdTask) + + onTaskOpening(task) + onTaskOpening(secondTask) + onTaskOpening(thirdTask) + + val wct = mock<WindowContainerTransaction>() + displayChangingListener.onDisplayChange( + task.displayId, Surface.ROTATION_0, Surface.ROTATION_90, null, wct + ) + + verify(wct).setBounds(eq(task.token), any()) + verify(wct, never()).setBounds(eq(secondTask.token), any()) + verify(wct, never()).setBounds(eq(thirdTask.token), any()) + } + + @Test + fun testCloseButtonInFreeform_closeWindow_ignoreMoveEventsWithoutBoundsChange() { + val onClickListenerCaptor = forClass(View.OnClickListener::class.java) + as ArgumentCaptor<View.OnClickListener> + val onTouchListenerCaptor = forClass(View.OnTouchListener::class.java) + as ArgumentCaptor<View.OnTouchListener> + val decor = createOpenTaskDecoration( + windowingMode = WINDOWING_MODE_FREEFORM, + onCaptionButtonClickListener = onClickListenerCaptor, + onCaptionButtonTouchListener = onTouchListenerCaptor + ) + + whenever(mockTaskPositioner.onDragPositioningStart(any(), any(), any())) + .thenReturn(INITIAL_BOUNDS) + whenever(mockTaskPositioner.onDragPositioningMove(any(), any())) + .thenReturn(INITIAL_BOUNDS) + whenever(mockTaskPositioner.onDragPositioningEnd(any(), any())) + .thenReturn(INITIAL_BOUNDS) + + val view = mock(View::class.java) + whenever(view.id).thenReturn(R.id.close_window) + val viewRootImpl = mock(ViewRootImpl::class.java) + whenever(view.getViewRootImpl()).thenReturn(viewRootImpl) + whenever(viewRootImpl.getInputToken()).thenReturn(null) + + desktopModeWindowDecorViewModel + .setFreeformTaskTransitionStarter(mockFreeformTaskTransitionStarter) + + onTouchListenerCaptor.value.onTouch(view, + MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), + MotionEvent.ACTION_DOWN, /* x= */ 0f, /* y= */ 0f, /* metaState= */ 0)) + onTouchListenerCaptor.value.onTouch(view, + MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), + MotionEvent.ACTION_MOVE, /* x= */ 0f, /* y= */ 0f, /* metaState= */ 0)) + onTouchListenerCaptor.value.onTouch(view, + MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), + MotionEvent.ACTION_UP, /* x= */ 0f, /* y= */ 0f, /* metaState= */ 0)) + onClickListenerCaptor.value.onClick(view) + + val transactionCaptor = argumentCaptor<WindowContainerTransaction>() + verify(mockFreeformTaskTransitionStarter).startRemoveTransition(transactionCaptor.capture()) + val wct = transactionCaptor.firstValue + + assertEquals(1, wct.getHierarchyOps().size) + assertEquals(HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK, + wct.getHierarchyOps().get(0).getType()) + assertEquals(decor.mTaskInfo.token.asBinder(), wct.getHierarchyOps().get(0).getContainer()) + } + + private fun createOpenTaskDecoration( + @WindowingMode windowingMode: Int, + taskSurface: SurfaceControl = SurfaceControl(), + onMaxOrRestoreListenerCaptor: ArgumentCaptor<Function0<Unit>> = + forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>, + onLeftSnapClickListenerCaptor: ArgumentCaptor<Function0<Unit>> = + forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>, + onRightSnapClickListenerCaptor: ArgumentCaptor<Function0<Unit>> = + forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>, + onToDesktopClickListenerCaptor: ArgumentCaptor<Consumer<DesktopModeTransitionSource>> = + forClass(Consumer::class.java) as ArgumentCaptor<Consumer<DesktopModeTransitionSource>>, + onToFullscreenClickListenerCaptor: ArgumentCaptor<Function0<Unit>> = + forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>, + onToSplitScreenClickListenerCaptor: ArgumentCaptor<Function0<Unit>> = + forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>, + onOpenInBrowserClickListener: ArgumentCaptor<Consumer<Uri>> = + forClass(Consumer::class.java) as ArgumentCaptor<Consumer<Uri>>, + onCaptionButtonClickListener: ArgumentCaptor<View.OnClickListener> = + forClass(View.OnClickListener::class.java) as ArgumentCaptor<View.OnClickListener>, + onCaptionButtonTouchListener: ArgumentCaptor<View.OnTouchListener> = + forClass(View.OnTouchListener::class.java) as ArgumentCaptor<View.OnTouchListener> + ): DesktopModeWindowDecoration { + val decor = setUpMockDecorationForTask(createTask(windowingMode = windowingMode)) + onTaskOpening(decor.mTaskInfo, taskSurface) + verify(decor).setOnMaximizeOrRestoreClickListener(onMaxOrRestoreListenerCaptor.capture()) + verify(decor).setOnLeftSnapClickListener(onLeftSnapClickListenerCaptor.capture()) + verify(decor).setOnRightSnapClickListener(onRightSnapClickListenerCaptor.capture()) + verify(decor).setOnToDesktopClickListener(onToDesktopClickListenerCaptor.capture()) + verify(decor).setOnToFullscreenClickListener(onToFullscreenClickListenerCaptor.capture()) + verify(decor).setOnToSplitScreenClickListener(onToSplitScreenClickListenerCaptor.capture()) + verify(decor).setOpenInBrowserClickListener(onOpenInBrowserClickListener.capture()) + verify(decor).setCaptionListeners( + onCaptionButtonClickListener.capture(), onCaptionButtonTouchListener.capture(), + any(), any()) + return decor } private fun onTaskOpening(task: RunningTaskInfo, leash: SurfaceControl = SurfaceControl()) { @@ -538,10 +1195,10 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { private fun createTask( displayId: Int = DEFAULT_DISPLAY, - @WindowConfiguration.WindowingMode windowingMode: Int, + @WindowingMode windowingMode: Int, activityType: Int = ACTIVITY_TYPE_STANDARD, focused: Boolean = true, - activityInfo: ActivityInfo = ActivityInfo() + activityInfo: ActivityInfo = ActivityInfo(), ): RunningTaskInfo { return TestRunningTaskInfoBuilder() .setDisplayId(displayId) @@ -551,6 +1208,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { .build().apply { topActivityInfo = activityInfo isFocused = focused + isResizeable = true } } @@ -558,10 +1216,17 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { val decoration = mock(DesktopModeWindowDecoration::class.java) whenever( mockDesktopModeWindowDecorFactory.create( - any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), + any(), any(), any(), any(), any(), any(), any()) ).thenReturn(decoration) decoration.mTaskInfo = task whenever(decoration.isFocused).thenReturn(task.isFocused) + whenever(decoration.user).thenReturn(mockUserHandle) + if (task.windowingMode == WINDOWING_MODE_MULTI_WINDOW) { + whenever(mockSplitScreenController.isTaskInSplitScreen(task.taskId)) + .thenReturn(true) + } + whenever(decoration.calculateValidDragArea()).thenReturn(Rect(0, 60, 2560, 1600)) return decoration } @@ -582,7 +1247,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { ) } - private fun RunningTaskInfo.setWindowingMode(@WindowConfiguration.WindowingMode mode: Int) { + private fun RunningTaskInfo.setWindowingMode(@WindowingMode mode: Int) { configuration.windowConfiguration.windowingMode = mode } @@ -593,5 +1258,6 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { companion object { private const val TAG = "DesktopModeWindowDecorViewModelTests" private val STABLE_INSETS = Rect(0, 100, 0, 0) + private val INITIAL_BOUNDS = Rect(0, 0, 100, 100) } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 46c158908226..9c11ec34ef44 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -19,16 +19,28 @@ package com.android.wm.shell.windowdecor; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; +import static android.view.InsetsSource.FLAG_FORCE_CONSUMING; +import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR; +import static android.view.WindowInsets.Type.statusBars; import static android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceControlTransaction; +import static com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.CLOSE_MAXIMIZE_MENU_DELAY_MS; import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -37,12 +49,17 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.app.assist.AssistContent; import android.content.ComponentName; +import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.PointF; +import android.graphics.Rect; +import android.net.Uri; import android.os.Handler; import android.os.SystemProperties; import android.platform.test.annotations.DisableFlags; @@ -50,18 +67,22 @@ import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; +import android.testing.TestableLooper; import android.view.AttachedSurfaceControl; import android.view.Choreographer; import android.view.Display; import android.view.GestureDetector; +import android.view.InsetsSource; import android.view.InsetsState; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; import android.view.View; +import android.view.WindowInsets; import android.view.WindowManager; import android.window.WindowContainerTransaction; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; @@ -72,21 +93,39 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser; +import com.android.wm.shell.apptoweb.AssistContentRequester; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.MultiInstanceHelper; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.shared.DesktopModeStatus; +import com.android.wm.shell.desktopmode.CaptionState; +import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams; +import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; +import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHost; +import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier; + +import kotlin.Unit; +import kotlin.jvm.functions.Function0; +import kotlin.jvm.functions.Function1; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.quality.Strictness; +import java.util.function.Consumer; import java.util.function.Supplier; /** @@ -96,6 +135,7 @@ import java.util.function.Supplier; * atest WMShellUnitTests:DesktopModeWindowDecorationTests */ @SmallTest +@TestableLooper.RunWithLooper @RunWith(AndroidTestingRunner.class) public class DesktopModeWindowDecorationTests extends ShellTestCase { private static final String USE_WINDOW_SHADOWS_SYSPROP_KEY = @@ -105,19 +145,27 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private static final String USE_ROUNDED_CORNERS_SYSPROP_KEY = "persist.wm.debug.desktop_use_rounded_corners"; + private static final Uri TEST_URI1 = Uri.parse("https://www.google.com/"); + private static final Uri TEST_URI2 = Uri.parse("https://docs.google.com/"); + private static final Uri TEST_URI3 = Uri.parse("https://slides.google.com/"); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); @Mock private DisplayController mMockDisplayController; @Mock - private ShellTaskOrganizer mMockShellTaskOrganizer; + private SplitScreenController mMockSplitScreenController; @Mock - private Handler mMockHandler; + private ShellTaskOrganizer mMockShellTaskOrganizer; @Mock private Choreographer mMockChoreographer; @Mock private SyncTransactionQueue mMockSyncQueue; @Mock + private AppHeaderViewHolder.Factory mMockAppHeaderViewHolderFactory; + @Mock + private AppHeaderViewHolder mMockAppHeaderViewHolder; + @Mock private RootTaskDisplayAreaOrganizer mMockRootTaskDisplayAreaOrganizer; @Mock private Supplier<SurfaceControl.Transaction> mMockTransactionSupplier; @@ -130,19 +178,46 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @Mock private WindowDecoration.SurfaceControlViewHostFactory mMockSurfaceControlViewHostFactory; @Mock + private WindowDecorViewHostSupplier mMockWindowDecorViewHostSupplier; + @Mock + private WindowDecorViewHost mMockWindowDecorViewHost; + @Mock private TypedArray mMockRoundedCornersRadiusArray; - @Mock private TestTouchEventListener mMockTouchEventListener; @Mock private DesktopModeWindowDecoration.ExclusionRegionListener mMockExclusionRegionListener; @Mock private PackageManager mMockPackageManager; + @Mock + private Handler mMockHandler; + @Mock + private Consumer<Uri> mMockOpenInBrowserClickListener; + @Mock + private AppToWebGenericLinksParser mMockGenericLinksParser; + @Mock + private WindowManager mMockWindowManager; + @Mock + private AssistContentRequester mMockAssistContentRequester; + @Mock + private HandleMenu mMockHandleMenu; + @Mock + private HandleMenuFactory mMockHandleMenuFactory; + @Mock + private MultiInstanceHelper mMockMultiInstanceHelper; + @Mock + private WindowDecorCaptionHandleRepository mMockCaptionHandleRepository; + @Captor + private ArgumentCaptor<Function1<Boolean, Unit>> mOnMaxMenuHoverChangeListener; + @Captor + private ArgumentCaptor<Runnable> mCloseMaxMenuRunnable; private final InsetsState mInsetsState = new InsetsState(); private SurfaceControl.Transaction mMockTransaction; private StaticMockitoSession mMockitoSession; private TestableContext mTestableContext; + private final ShellExecutor mBgExecutor = new TestShellExecutor(); + private final AssistContent mAssistContent = new AssistContent(); /** Set up run before test class. */ @BeforeClass @@ -154,7 +229,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Before - public void setUp() { + public void setUp() throws PackageManager.NameNotFoundException { mMockitoSession = mockitoSession() .strictness(Strictness.LENIENT) .spyStatic(DesktopModeStatus.class) @@ -169,10 +244,24 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { mTestableContext = new TestableContext(mContext); mTestableContext.ensureTestableResources(); mContext.setMockPackageManager(mMockPackageManager); + when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any())) + .thenReturn(false); when(mMockPackageManager.getApplicationLabel(any())).thenReturn("applicationLabel"); + final ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.applicationInfo = new ApplicationInfo(); + when(mMockPackageManager.getActivityInfo(any(), anyInt())).thenReturn(activityInfo); final Display defaultDisplay = mock(Display.class); doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY); doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt()); + when(mMockHandleMenuFactory.create(any(), any(), anyInt(), any(), any(), any(), + anyBoolean(), anyBoolean(), anyBoolean(), any(), anyInt(), anyInt(), anyInt())) + .thenReturn(mMockHandleMenu); + when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any())).thenReturn(false); + when(mMockWindowDecorViewHostSupplier.acquire(any(), eq(defaultDisplay))) + .thenReturn(mMockWindowDecorViewHost); + when(mMockWindowDecorViewHost.getSurfaceControl()).thenReturn(mock(SurfaceControl.class)); + when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any(), any(), + any())).thenReturn(mMockAppHeaderViewHolder); } @After @@ -335,6 +424,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + @DisableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX) public void updateRelayoutParams_fullscreen_inputChannelNotNeeded() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); @@ -351,6 +441,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + @DisableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX) public void updateRelayoutParams_multiwindow_inputChannelNotNeeded() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); @@ -367,6 +458,117 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + @EnableFlags(Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION) + public void updateRelayoutParams_defaultHeader_addsForceConsumingFlag() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance(0); + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false); + + assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) != 0).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION) + public void updateRelayoutParams_customHeader_noForceConsumptionFlag() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance( + APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND); + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false); + + assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) == 0).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS) + public void updateRelayoutParams_header_addsForceConsumingCaptionBar() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false); + + assertThat( + (relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) != 0) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS) + public void updateRelayoutParams_handle_skipsForceConsumingCaptionBar() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false); + + assertThat( + (relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) == 0) + .isTrue(); + } + + @Test + public void updateRelayoutParams_handle_requestsAsyncViewHostRendering() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + // Make the task fullscreen so that its decoration is an App Handle. + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false); + + // App Handles don't need to be rendered in sync with the task animation, per UX. + assertThat(relayoutParams.mAsyncViewHost).isTrue(); + } + + @Test + public void updateRelayoutParams_header_requestsSyncViewHostRendering() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + // Make the task freeform so that its decoration is an App Header. + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + /* applyStartTransactionOnDraw= */ true, + /* shouldSetTaskPositionAndCrop */ false); + + // App Headers must be rendered in sync with the task animation, so it cannot be delayed. + assertThat(relayoutParams.mAsyncViewHost).isFalse(); + } + + @Test public void relayout_fullscreenTask_appliesTransactionImmediately() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); @@ -379,6 +581,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + @Ignore("TODO(b/367235906): Due to MONITOR_INPUT permission error") public void relayout_freeformTask_appliesTransactionOnDraw() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); @@ -389,74 +592,419 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { spyWindowDecor.relayout(taskInfo); verify(mMockTransaction, never()).apply(); - verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockTransaction); + verify(mMockWindowDecorViewHost).updateView(any(), any(), any(), eq(mMockTransaction)); + } + + @Test + public void createMaximizeMenu_showsMenu() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + assertFalse(decoration.isMaximizeMenuActive()); + + createMaximizeMenu(decoration, menu); + + assertTrue(decoration.isMaximizeMenuActive()); + } + + @Test + public void maximizeMenu_unHoversMenu_schedulesCloseMenu() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + decoration.setAppHeaderMaximizeButtonHovered(false); + createMaximizeMenu(decoration, menu); + + mOnMaxMenuHoverChangeListener.getValue().invoke(false); + + verify(mMockHandler) + .postDelayed(mCloseMaxMenuRunnable.capture(), eq(CLOSE_MAXIMIZE_MENU_DELAY_MS)); + + mCloseMaxMenuRunnable.getValue().run(); + verify(menu).close(any()); + assertFalse(decoration.isMaximizeMenuActive()); + } + + @Test + public void maximizeMenu_unHoversButton_schedulesCloseMenu() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + decoration.setAppHeaderMaximizeButtonHovered(true); + createMaximizeMenu(decoration, menu); + + decoration.setAppHeaderMaximizeButtonHovered(false); + + verify(mMockHandler) + .postDelayed(mCloseMaxMenuRunnable.capture(), eq(CLOSE_MAXIMIZE_MENU_DELAY_MS)); + + mCloseMaxMenuRunnable.getValue().run(); + verify(menu).close(any()); + assertFalse(decoration.isMaximizeMenuActive()); } @Test - public void relayout_fullscreenTask_doesNotCreateViewHostImmediately() { + public void maximizeMenu_hoversMenu_cancelsCloseMenu() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + createMaximizeMenu(decoration, menu); + + mOnMaxMenuHoverChangeListener.getValue().invoke(true); + + verify(mMockHandler).removeCallbacks(any()); + } + + @Test + public void maximizeMenu_hoversButton_cancelsCloseMenu() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + createMaximizeMenu(decoration, menu); + + decoration.setAppHeaderMaximizeButtonHovered(true); + + verify(mMockHandler).removeCallbacks(any()); + } + + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) + public void capturedLink_handleMenuBrowserLinkSetToCapturedLinkIfValid() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final DesktopModeWindowDecoration decor = createWindowDecoration( + taskInfo, TEST_URI1 /* captured link */, TEST_URI2 /* web uri */, + TEST_URI3 /* generic link */); + + // Verify handle menu's browser link set as captured link + createHandleMenu(decor); + verifyHandleMenuCreated(TEST_URI1); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) + public void capturedLink_postsOnCapturedLinkExpiredRunnable() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final DesktopModeWindowDecoration decor = createWindowDecoration( + taskInfo, TEST_URI1 /* captured link */, null /* web uri */, + null /* generic link */); + final ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); + + // Run runnable to set captured link to expired + verify(mMockHandler).postDelayed(runnableArgument.capture(), anyLong()); + runnableArgument.getValue().run(); + + // Verify captured link is no longer valid by verifying link is not set as handle menu + // browser link. + createHandleMenu(decor); + verifyHandleMenuCreated(null /* uri */); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) + public void capturedLink_capturedLinkNotResetToSameLink() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final DesktopModeWindowDecoration decor = createWindowDecoration( + taskInfo, TEST_URI1 /* captured link */, null /* web uri */, + null /* generic link */); + final ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); + + // Run runnable to set captured link to expired + verify(mMockHandler).postDelayed(runnableArgument.capture(), anyLong()); + runnableArgument.getValue().run(); + + // Relayout decor with same captured link + decor.relayout(taskInfo); + + // Verify handle menu's browser link not set to captured link since link is expired + createHandleMenu(decor); + verifyHandleMenuCreated(null /* uri */); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) + public void capturedLink_capturedLinkStillUsedIfExpiredAfterHandleMenuCreation() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final DesktopModeWindowDecoration decor = createWindowDecoration( + taskInfo, TEST_URI1 /* captured link */, null /* web uri */, + null /* generic link */); + final ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); + + // Create handle menu before link expires + createHandleMenu(decor); + + // Run runnable to set captured link to expired + verify(mMockHandler).postDelayed(runnableArgument.capture(), anyLong()); + runnableArgument.getValue().run(); + + // Verify handle menu's browser link is set to captured link since menu was opened before + // captured link expired + createHandleMenu(decor); + verifyHandleMenuCreated(TEST_URI1); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) + public void capturedLink_capturedLinkExpiresAfterClick() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final DesktopModeWindowDecoration decor = createWindowDecoration( + taskInfo, TEST_URI1 /* captured link */, null /* web uri */, + null /* generic link */); + final ArgumentCaptor<Function1<Uri, Unit>> openInBrowserCaptor = + ArgumentCaptor.forClass(Function1.class); + + // Simulate menu opening and clicking open in browser button + createHandleMenu(decor); + verify(mMockHandleMenu).show( + any(), + any(), + any(), + any(), + any(), + openInBrowserCaptor.capture(), + any(), + any() + ); + openInBrowserCaptor.getValue().invoke(TEST_URI1); + + // Verify handle menu's browser link not set to captured link since link not valid after + // open in browser clicked + createHandleMenu(decor); + verifyHandleMenuCreated(null /* uri */); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) + public void capturedLink_openInBrowserListenerCalledOnClick() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final DesktopModeWindowDecoration decor = createWindowDecoration( + taskInfo, TEST_URI1 /* captured link */, null /* web uri */, + null /* generic link */); + final ArgumentCaptor<Function1<Uri, Unit>> openInBrowserCaptor = + ArgumentCaptor.forClass(Function1.class); + createHandleMenu(decor); + verify(mMockHandleMenu).show( + any(), + any(), + any(), + any(), + any(), + openInBrowserCaptor.capture(), + any(), + any() + ); + + openInBrowserCaptor.getValue().invoke(TEST_URI1); + + verify(mMockOpenInBrowserClickListener).accept(TEST_URI1); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) + public void webUriLink_webUriLinkUsedWhenCapturedLinkUnavailable() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final DesktopModeWindowDecoration decor = createWindowDecoration( + taskInfo, null /* captured link */, TEST_URI2 /* web uri */, + TEST_URI3 /* generic link */); + // Verify handle menu's browser link set as web uri link when captured link is unavailable + createHandleMenu(decor); + verifyHandleMenuCreated(TEST_URI2); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) + public void genericLink_genericLinkUsedWhenCapturedLinkAndWebUriUnavailable() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final DesktopModeWindowDecoration decor = createWindowDecoration( + taskInfo, null /* captured link */, null /* web uri */, + TEST_URI3 /* generic link */); + + // Verify handle menu's browser link set as generic link when captured link and web uri link + // are unavailable + createHandleMenu(decor); + verifyHandleMenuCreated(TEST_URI3); + } + + @Test + public void handleMenu_onCloseMenuClick_closesMenu() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + true /* relayout */); + final ArgumentCaptor<Function0<Unit>> closeClickListener = + ArgumentCaptor.forClass(Function0.class); + createHandleMenu(decoration); + verify(mMockHandleMenu).show( + any(), + any(), + any(), + any(), + any(), + any(), + closeClickListener.capture(), + any() + ); + + closeClickListener.getValue().invoke(); + + verify(mMockHandleMenu).close(); + assertFalse(decoration.isHandleMenuActive()); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + public void notifyCaptionStateChanged_flagDisabled_doNoNotify() { + when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true); final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + when(mMockDisplayController.getInsetsState(taskInfo.displayId)) + .thenReturn(createInsetsState(statusBars(), /* visible= */true)); final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); spyWindowDecor.relayout(taskInfo); - verify(mMockSurfaceControlViewHostFactory, never()).create(any(), any(), any()); + verify(mMockCaptionHandleRepository, never()).notifyCaptionChanged(any()); } @Test - public void relayout_fullscreenTask_postsViewHostCreation() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + public void notifyCaptionStateChanged_inFullscreenMode_notifiesAppHandleVisible() { + when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true); final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + when(mMockDisplayController.getInsetsState(taskInfo.displayId)) + .thenReturn(createInsetsState(statusBars(), /* visible= */true)); final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( + CaptionState.class); - ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); spyWindowDecor.relayout(taskInfo); - verify(mMockHandler).post(runnableArgument.capture()); - runnableArgument.getValue().run(); - verify(mMockSurfaceControlViewHostFactory).create(any(), any(), any()); + verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged( + captionStateArgumentCaptor.capture()); + assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf( + CaptionState.AppHandle.class); } @Test - public void relayout_freeformTask_createsViewHostImmediately() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + @Ignore("TODO(b/367235906): Due to MONITOR_INPUT permission error") + public void notifyCaptionStateChanged_inWindowingMode_notifiesAppHeaderVisible() { + when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true); final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + when(mMockDisplayController.getInsetsState(taskInfo.displayId)) + .thenReturn(createInsetsState(statusBars(), /* visible= */true)); + when(mMockAppHeaderViewHolder.getAppChipLocationInWindow()).thenReturn( + new Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3)); final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT) taskInfo.isResizeable = false; + ArgumentCaptor<Function0<Unit>> runnableArgumentCaptor = ArgumentCaptor.forClass( + Function0.class); + ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( + CaptionState.class); + + spyWindowDecor.relayout(taskInfo); + verify(mMockAppHeaderViewHolder, atLeastOnce()).runOnAppChipGlobalLayout( + runnableArgumentCaptor.capture()); + runnableArgumentCaptor.getValue().invoke(); + + verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged( + captionStateArgumentCaptor.capture()); + assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf( + CaptionState.AppHeader.class); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + public void notifyCaptionStateChanged_taskNotVisible_notifiesNoCaptionVisible() { + when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true); + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ false); + when(mMockDisplayController.getInsetsState(taskInfo.displayId)) + .thenReturn(createInsetsState(statusBars(), /* visible= */true)); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_UNDEFINED); + ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( + CaptionState.class); spyWindowDecor.relayout(taskInfo); - verify(mMockSurfaceControlViewHostFactory).create(any(), any(), any()); - verify(mMockHandler, never()).post(any()); + verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged( + captionStateArgumentCaptor.capture()); + assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf( + CaptionState.NoCaption.class); } @Test - public void relayout_removesExistingHandlerCallback() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + public void notifyCaptionStateChanged_captionHandleExpanded_notifiesHandleMenuExpanded() { + when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true); final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + when(mMockDisplayController.getInsetsState(taskInfo.displayId)) + .thenReturn(createInsetsState(statusBars(), /* visible= */true)); final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); - ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); - spyWindowDecor.relayout(taskInfo); - verify(mMockHandler).post(runnableArgument.capture()); + ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( + CaptionState.class); spyWindowDecor.relayout(taskInfo); - - verify(mMockHandler).removeCallbacks(runnableArgument.getValue()); + createHandleMenu(spyWindowDecor); + + verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged( + captionStateArgumentCaptor.capture()); + assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf( + CaptionState.AppHandle.class); + assertThat( + ((CaptionState.AppHandle) captionStateArgumentCaptor.getValue()) + .isHandleMenuExpanded()).isEqualTo( + true); } @Test - public void close_removesExistingHandlerCallback() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + public void notifyCaptionStateChanged_captionHandleClosed_notifiesHandleMenuClosed() { + when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true); final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + when(mMockDisplayController.getInsetsState(taskInfo.displayId)) + .thenReturn(createInsetsState(statusBars(), /* visible= */true)); final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); - ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); + ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( + CaptionState.class); + spyWindowDecor.relayout(taskInfo); - verify(mMockHandler).post(runnableArgument.capture()); + createHandleMenu(spyWindowDecor); + spyWindowDecor.closeHandleMenu(); + + verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged( + captionStateArgumentCaptor.capture()); + assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf( + CaptionState.AppHandle.class); + assertThat( + ((CaptionState.AppHandle) captionStateArgumentCaptor.getValue()) + .isHandleMenuExpanded()).isEqualTo( + false); - spyWindowDecor.close(); + } + + private void verifyHandleMenuCreated(@Nullable Uri uri) { + verify(mMockHandleMenuFactory).create(any(), any(), anyInt(), any(), any(), + any(), anyBoolean(), anyBoolean(), anyBoolean(), eq(uri), anyInt(), + anyInt(), anyInt()); + } - verify(mMockHandler).removeCallbacks(runnableArgument.getValue()); + private void createMaximizeMenu(DesktopModeWindowDecoration decoration, MaximizeMenu menu) { + final Function0<Unit> l = () -> Unit.INSTANCE; + decoration.setOnMaximizeOrRestoreClickListener(l); + decoration.setOnLeftSnapClickListener(l); + decoration.setOnRightSnapClickListener(l); + decoration.createMaximizeMenu(); + verify(menu).show(any(), any(), any(), mOnMaxMenuHoverChangeListener.capture(), any()); } private void fillRoundedCornersResources(int fillValue) { @@ -476,18 +1024,57 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { R.dimen.rounded_corner_radius_bottom, fillValue); } + private DesktopModeWindowDecoration createWindowDecoration( + ActivityManager.RunningTaskInfo taskInfo, @Nullable Uri capturedLink, + @Nullable Uri webUri, @Nullable Uri genericLink) { + taskInfo.capturedLink = capturedLink; + taskInfo.capturedLinkTimestamp = System.currentTimeMillis(); + mAssistContent.setWebUri(webUri); + final String genericLinkString = genericLink == null ? null : genericLink.toString(); + doReturn(genericLinkString).when(mMockGenericLinksParser).getGenericLink(any()); + // Relayout to set captured link + return createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory(), true /* relayout */); + } private DesktopModeWindowDecoration createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo) { - DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext, - mMockDisplayController, mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, - mMockHandler, mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer, - SurfaceControl.Builder::new, mMockTransactionSupplier, - WindowContainerTransaction::new, SurfaceControl::new, - mMockSurfaceControlViewHostFactory); + return createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory(), + false /* relayout */); + } + + private DesktopModeWindowDecoration createWindowDecoration( + ActivityManager.RunningTaskInfo taskInfo, boolean relayout) { + return createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory(), relayout); + } + + private DesktopModeWindowDecoration createWindowDecoration( + ActivityManager.RunningTaskInfo taskInfo, + MaximizeMenuFactory maximizeMenuFactory) { + return createWindowDecoration(taskInfo, maximizeMenuFactory, false /* relayout */); + } + + private DesktopModeWindowDecoration createWindowDecoration( + ActivityManager.RunningTaskInfo taskInfo, + MaximizeMenuFactory maximizeMenuFactory, + boolean relayout) { + final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext, + mContext, mMockDisplayController, mMockSplitScreenController, + mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mMockHandler, mBgExecutor, + mMockChoreographer, mMockSyncQueue, mMockAppHeaderViewHolderFactory, + mMockRootTaskDisplayAreaOrganizer, + mMockGenericLinksParser, mMockAssistContentRequester, SurfaceControl.Builder::new, + mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new, + new WindowManagerWrapper(mMockWindowManager), mMockSurfaceControlViewHostFactory, + mMockWindowDecorViewHostSupplier, maximizeMenuFactory, mMockHandleMenuFactory, + mMockMultiInstanceHelper, mMockCaptionHandleRepository); windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener, mMockTouchEventListener, mMockTouchEventListener); windowDecor.setExclusionRegionListener(mMockExclusionRegionListener); + windowDecor.setOpenInBrowserClickListener(mMockOpenInBrowserClickListener); + windowDecor.mDecorWindowContext = mContext; + if (relayout) { + windowDecor.relayout(taskInfo); + } return windowDecor; } @@ -499,8 +1086,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { .setTaskDescriptionBuilder(taskDescriptionBuilder) .setVisible(visible) .build(); - taskInfo.topActivityInfo = new ActivityInfo(); - taskInfo.topActivityInfo.applicationInfo = new ApplicationInfo(); taskInfo.realActivity = new ComponentName("com.android.wm.shell.windowdecor", "DesktopModeWindowDecorationTests"); taskInfo.baseActivity = new ComponentName("com.android.wm.shell.windowdecor", @@ -509,11 +1094,26 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } + private void createHandleMenu(@NonNull DesktopModeWindowDecoration decor) { + decor.createHandleMenu(false); + // Call DesktopModeWindowDecoration#onAssistContentReceived because decor waits to receive + // {@link AssistContent} before creating the menu + decor.onAssistContentReceived(mAssistContent); + } + private static boolean hasNoInputChannelFeature(RelayoutParams params) { return (params.mInputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) != 0; } + private InsetsState createInsetsState(@WindowInsets.Type.InsetsType int type, boolean visible) { + final InsetsState state = new InsetsState(); + final InsetsSource source = new InsetsSource(/* id= */0, type); + source.setVisible(visible); + state.addSource(source); + return state; + } + private static class TestTouchEventListener extends GestureDetector.SimpleOnGestureListener implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener, View.OnGenericMotionListener, DragDetector.MotionEventHandler { @@ -541,4 +1141,27 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { return false; } } + + private static final class FakeMaximizeMenuFactory implements MaximizeMenuFactory { + private final MaximizeMenu mMaximizeMenu; + + FakeMaximizeMenuFactory() { + this(mock(MaximizeMenu.class)); + } + + FakeMaximizeMenuFactory(MaximizeMenu menu) { + mMaximizeMenu = menu; + } + + @NonNull + @Override + public MaximizeMenu create(@NonNull SyncTransactionQueue syncQueue, + @NonNull RootTaskDisplayAreaOrganizer rootTdaOrganizer, + @NonNull DisplayController displayController, + @NonNull ActivityManager.RunningTaskInfo taskInfo, + @NonNull Context decorWindowContext, @NonNull PointF menuPosition, + @NonNull Supplier<SurfaceControl.Transaction> transactionSupplier) { + return mMaximizeMenu; + } + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt index 3fbab0f9e2bb..7f7211d65fde 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt @@ -21,6 +21,7 @@ import android.testing.AndroidTestingRunner import android.view.MotionEvent import android.view.InputDevice import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase import org.junit.After import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -34,6 +35,7 @@ import org.mockito.Mockito.any import org.mockito.Mockito.argThat import org.mockito.Mockito.never import org.mockito.Mockito.verify +import org.mockito.kotlin.times /** * Tests for [DragDetector]. @@ -43,22 +45,17 @@ import org.mockito.Mockito.verify */ @SmallTest @RunWith(AndroidTestingRunner::class) -class DragDetectorTest { +class DragDetectorTest : ShellTestCase() { private val motionEvents = mutableListOf<MotionEvent>() @Mock private lateinit var eventHandler: DragDetector.MotionEventHandler - private lateinit var dragDetector: DragDetector - @Before fun setUp() { MockitoAnnotations.initMocks(this) `when`(eventHandler.handleMotionEvent(any(), any())).thenReturn(true) - - dragDetector = DragDetector(eventHandler) - dragDetector.setTouchSlop(SLOP) } @After @@ -71,6 +68,7 @@ class DragDetectorTest { @Test fun testNoMove_passesDownAndUp() { + val dragDetector = createDragDetector() assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN))) verify(eventHandler).handleMotionEvent(any(), argThat { return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y && @@ -85,7 +83,26 @@ class DragDetectorTest { } @Test + fun testNoMove_mouse_passesDownAndUp() { + val dragDetector = createDragDetector() + assertTrue(dragDetector.onMotionEvent( + createMotionEvent(MotionEvent.ACTION_DOWN, isTouch = false))) + verify(eventHandler).handleMotionEvent(any(), argThat { + return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y && + it.source == InputDevice.SOURCE_MOUSE + }) + + assertTrue(dragDetector.onMotionEvent( + createMotionEvent(MotionEvent.ACTION_UP, isTouch = false))) + verify(eventHandler).handleMotionEvent(any(), argThat { + return@argThat it.action == MotionEvent.ACTION_UP && it.x == X && it.y == Y && + it.source == InputDevice.SOURCE_MOUSE + }) + } + + @Test fun testMoveInSlop_touch_passesDownAndUp() { + val dragDetector = createDragDetector() `when`(eventHandler.handleMotionEvent(any(), argThat { return@argThat it.action == MotionEvent.ACTION_DOWN })).thenReturn(false) @@ -112,6 +129,7 @@ class DragDetectorTest { @Test fun testMoveInSlop_mouse_passesDownMoveAndUp() { + val dragDetector = createDragDetector() `when`(eventHandler.handleMotionEvent(any(), argThat { it.action == MotionEvent.ACTION_DOWN })).thenReturn(false) @@ -141,6 +159,7 @@ class DragDetectorTest { @Test fun testMoveBeyondSlop_passesDownMoveAndUp() { + val dragDetector = createDragDetector() `when`(eventHandler.handleMotionEvent(any(), argThat { it.action == MotionEvent.ACTION_DOWN })).thenReturn(false) @@ -166,7 +185,56 @@ class DragDetectorTest { } @Test + fun testDownMoveDown_shouldIgnoreTheSecondDownMotion() { + val dragDetector = createDragDetector() + assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN))) + verify(eventHandler).handleMotionEvent(any(), argThat { + return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y && + it.source == InputDevice.SOURCE_TOUCHSCREEN + }) + + val newX = X + SLOP + 1 + assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y))) + verify(eventHandler).handleMotionEvent(any(), argThat { + return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y && + it.source == InputDevice.SOURCE_TOUCHSCREEN + }) + + assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN))) + verify(eventHandler).handleMotionEvent(any(), argThat { + return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y && + it.source == InputDevice.SOURCE_TOUCHSCREEN + }) + } + + @Test + fun testDownMouseMoveDownTouch_shouldIgnoreTheTouchDownMotion() { + val dragDetector = createDragDetector() + assertTrue(dragDetector.onMotionEvent( + createMotionEvent(MotionEvent.ACTION_DOWN, isTouch = false))) + verify(eventHandler).handleMotionEvent(any(), argThat { + return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y && + it.source == InputDevice.SOURCE_MOUSE + }) + + val newX = X + SLOP + 1 + assertTrue(dragDetector.onMotionEvent( + createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y, isTouch = false))) + verify(eventHandler).handleMotionEvent(any(), argThat { + return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y && + it.source == InputDevice.SOURCE_MOUSE + }) + + assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN))) + verify(eventHandler).handleMotionEvent(any(), argThat { + return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y && + it.source == InputDevice.SOURCE_MOUSE + }) + } + + @Test fun testPassesHoverEnter() { + val dragDetector = createDragDetector() `when`(eventHandler.handleMotionEvent(any(), argThat { it.action == MotionEvent.ACTION_HOVER_ENTER })).thenReturn(false) @@ -179,6 +247,7 @@ class DragDetectorTest { @Test fun testPassesHoverMove() { + val dragDetector = createDragDetector() assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_MOVE))) verify(eventHandler).handleMotionEvent(any(), argThat { return@argThat it.action == MotionEvent.ACTION_HOVER_MOVE && it.x == X && it.y == Y @@ -187,21 +256,240 @@ class DragDetectorTest { @Test fun testPassesHoverExit() { + val dragDetector = createDragDetector() assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_EXIT))) verify(eventHandler).handleMotionEvent(any(), argThat { return@argThat it.action == MotionEvent.ACTION_HOVER_EXIT && it.x == X && it.y == Y }) } - private fun createMotionEvent(action: Int, x: Float = X, y: Float = Y, isTouch: Boolean = true): - MotionEvent { - val time = SystemClock.uptimeMillis() - val ev = MotionEvent.obtain(time, time, action, x, y, 0) + @Test + fun testHoldToDrag_holdsWithMovementWithinSlop_passesDragMoveEvents() { + val dragDetector = createDragDetector(holdToDragMinDurationMs = 100, slop = 20) + val downTime = SystemClock.uptimeMillis() + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_DOWN, + x = 500f, + y = 10f, + isTouch = true, + downTime = downTime, + eventTime = downTime + )) + + // Couple of movements within the slop, still counting as "holding" + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f + 10f, // within slop + y = 10f + 10f, // within slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 30 + )) + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f - 10f, // within slop + y = 10f - 5f, // within slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 70 + )) + // Now go beyond slop, but after the required holding period. + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f + 50f, // beyond slop + y = 10f + 50f, // beyond slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 101 // after hold period + )) + + // Had a valid hold, so there should be 1 "move". + verify(eventHandler, times(1)) + .handleMotionEvent(any(), argThat { ev -> ev.action == MotionEvent.ACTION_MOVE }) + } + + @Test + fun testHoldToDrag_holdsWithoutAnyMovement_passesMoveEvents() { + val dragDetector = createDragDetector(holdToDragMinDurationMs = 100, slop = 20) + val downTime = SystemClock.uptimeMillis() + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_DOWN, + x = 500f, + y = 10f, + isTouch = true, + downTime = downTime, + eventTime = downTime + )) + + // First |move| is already beyond slop and after holding period. + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f + 50f, // beyond slop + y = 10f + 50f, // beyond slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 101 // after hold period + )) + + // Considered a valid hold, so there should be 1 "move". + verify(eventHandler, times(1)) + .handleMotionEvent(any(), argThat { ev -> ev.action == MotionEvent.ACTION_MOVE }) + } + + @Test + fun testHoldToDrag_returnsWithinSlopAfterHoldPeriod_passesDragMoveEvents() { + val dragDetector = createDragDetector(holdToDragMinDurationMs = 100, slop = 20) + val downTime = SystemClock.uptimeMillis() + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_DOWN, + x = 500f, + y = 10f, + isTouch = true, + downTime = downTime, + eventTime = downTime + )) + // Go beyond slop after the required holding period. + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f + 50f, // beyond slop + y = 10f + 50f, // beyond slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 101 // after hold period + )) + + // Return to original coordinates after holding period. + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f, // within slop + y = 10f, // within slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 102 // after hold period + )) + + // Both |moves| should be passed, even the one in the slop region since it was after the + // holding period. (e.g. after you drag the handle you may return to its original position). + verify(eventHandler, times(2)) + .handleMotionEvent(any(), argThat { ev -> ev.action == MotionEvent.ACTION_MOVE }) + } + + @Test + fun testHoldToDrag_straysDuringHoldPeriod_skipsMoveEvents() { + val dragDetector = createDragDetector(holdToDragMinDurationMs = 100, slop = 20) + val downTime = SystemClock.uptimeMillis() + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_DOWN, + x = 500f, + y = 10f, + isTouch = true, + downTime = downTime, + eventTime = downTime + )) + + // Go beyond slop before the required holding period. + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f + 50f, // beyond slop + y = 10f + 50f, // beyond slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 30 // during hold period + )) + + // The |move| was too quick and did not held, do not pass it to the handler. + verify(eventHandler, never()) + .handleMotionEvent(any(), argThat { ev -> ev.action == MotionEvent.ACTION_MOVE }) + } + + @Test + fun testHoldToDrag_straysDuringHoldPeriodAndReturnsWithinSlop_skipsMoveEvents() { + val dragDetector = createDragDetector(holdToDragMinDurationMs = 100, slop = 20) + val downTime = SystemClock.uptimeMillis() + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_DOWN, + x = 500f, + y = 10f, + isTouch = true, + downTime = downTime, + eventTime = downTime + )) + // Go beyond slop before the required holding period. + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f + 50f, // beyond slop + y = 10f + 50f, // beyond slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 30 // during hold period + )) + + // Return to slop area during holding period. + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f + 10f, // within slop + y = 10f + 10f, // within slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 50 // during hold period + )) + + // The first |move| invalidates the drag even if you return within the hold period, so the + // |move| should not be passed to the handler. + verify(eventHandler, never()) + .handleMotionEvent(any(), argThat { ev -> ev.action == MotionEvent.ACTION_MOVE }) + } + + @Test + fun testHoldToDrag_noHoldRequired_passesMoveEvents() { + val dragDetector = createDragDetector(holdToDragMinDurationMs = 0, slop = 20) + val downTime = SystemClock.uptimeMillis() + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_DOWN, + x = 500f, + y = 10f, + isTouch = true, + downTime = downTime, + eventTime = downTime + )) + + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f + 50f, // beyond slop + y = 10f + 50f, // beyond slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 1 + )) + + // The |move| should be passed to the handler as no hold period was needed. + verify(eventHandler, times(1)) + .handleMotionEvent(any(), argThat { ev -> ev.action == MotionEvent.ACTION_MOVE }) + } + + private fun createMotionEvent( + action: Int, + x: Float = X, + y: Float = Y, + isTouch: Boolean = true, + downTime: Long = SystemClock.uptimeMillis(), + eventTime: Long = SystemClock.uptimeMillis() + ): MotionEvent { + val ev = MotionEvent.obtain(downTime, eventTime, action, x, y, 0) ev.source = if (isTouch) InputDevice.SOURCE_TOUCHSCREEN else InputDevice.SOURCE_MOUSE motionEvents.add(ev) return ev } + private fun createDragDetector( + holdToDragMinDurationMs: Long = 0, + slop: Int = SLOP + ) = DragDetector( + eventHandler, + holdToDragMinDurationMs, + slop + ) + companion object { private const val SLOP = 10 private const val X = 123f diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt index f750e6b9a6fe..24f6becc3536 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt @@ -21,27 +21,36 @@ import android.content.res.Resources import android.graphics.PointF import android.graphics.Rect import android.os.IBinder +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display import android.window.WindowContainerToken +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn +import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout -import com.android.wm.shell.shared.DesktopModeStatus +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP import com.google.common.truth.Truth.assertThat +import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue +import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.any import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations +import org.mockito.quality.Strictness /** * Tests for [DragPositioningCallbackUtility]. @@ -53,24 +62,39 @@ import org.mockito.MockitoAnnotations class DragPositioningCallbackUtilityTest { @Mock private lateinit var mockWindowDecoration: WindowDecoration<*> + @Mock private lateinit var taskToken: WindowContainerToken + @Mock private lateinit var taskBinder: IBinder + @Mock private lateinit var mockDisplayController: DisplayController + @Mock private lateinit var mockDisplayLayout: DisplayLayout + @Mock private lateinit var mockDisplay: Display + @Mock private lateinit var mockContext: Context + @Mock private lateinit var mockResources: Resources + @JvmField + @Rule + val setFlagsRule = SetFlagsRule() + + private lateinit var mockitoSession: StaticMockitoSession + @Before fun setup() { MockitoAnnotations.initMocks(this) + mockitoSession = ExtendedMockito.mockitoSession().strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java).startMocking() whenever(taskToken.asBinder()).thenReturn(taskBinder) whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout) @@ -82,15 +106,21 @@ class DragPositioningCallbackUtilityTest { initializeTaskInfo() mockWindowDecoration.mDisplay = mockDisplay mockWindowDecoration.mDecorWindowContext = mockContext + mockWindowDecoration.mTaskInfo.isResizeable = true whenever(mockContext.getResources()).thenReturn(mockResources) whenever(mockWindowDecoration.mDecorWindowContext.resources).thenReturn(mockResources) whenever(mockResources.getDimensionPixelSize(R.dimen.desktop_mode_minimum_window_width)) - .thenReturn(DESKTOP_MODE_MIN_WIDTH) + .thenReturn(DESKTOP_MODE_MIN_WIDTH) whenever(mockResources.getDimensionPixelSize(R.dimen.desktop_mode_minimum_window_height)) - .thenReturn(DESKTOP_MODE_MIN_HEIGHT) + .thenReturn(DESKTOP_MODE_MIN_HEIGHT) whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID } } + @After + fun tearDown() { + mockitoSession.finishMocking() + } + @Test fun testChangeBoundsDoesNotChangeHeightWhenLessThanMin() { val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat()) @@ -101,9 +131,11 @@ class DragPositioningCallbackUtilityTest { val newY = STARTING_BOUNDS.top.toFloat() + 95 val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) - DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, - mockWindowDecoration) + mockWindowDecoration + ) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) @@ -121,9 +153,11 @@ class DragPositioningCallbackUtilityTest { val newY = STARTING_BOUNDS.top.toFloat() + 5 val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) - DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, - mockWindowDecoration) + mockWindowDecoration + ) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top + 5) @@ -132,6 +166,60 @@ class DragPositioningCallbackUtilityTest { } @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING) + fun testChangeBounds_unresizeableApp_heightLessThanMin_resetToStartingBounds() { + mockWindowDecoration.mTaskInfo.isResizeable = false + val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat()) + val repositionTaskBounds = Rect(STARTING_BOUNDS) + + // Resize to width of 95px and height of 5px with min width of 10px + val newX = STARTING_BOUNDS.right.toFloat() - 5 + val newY = STARTING_BOUNDS.top.toFloat() + 95 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + assertFalse( + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration + ) + ) + + assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right) + assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING) + fun testChangeBounds_unresizeableApp_widthLessThanMin_resetToStartingBounds() { + mockWindowDecoration.mTaskInfo.isResizeable = false + val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat()) + val repositionTaskBounds = Rect(STARTING_BOUNDS) + + // Resize to height of 95px and width of 5px with min width of 10px + val newX = STARTING_BOUNDS.right.toFloat() - 95 + val newY = STARTING_BOUNDS.top.toFloat() + 5 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + assertFalse( + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration + ) + ) + + + assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right) + assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom) + } + + + @Test fun testChangeBoundsDoesNotChangeHeightWhenNegative() { val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat()) val repositionTaskBounds = Rect(STARTING_BOUNDS) @@ -141,9 +229,11 @@ class DragPositioningCallbackUtilityTest { val newY = STARTING_BOUNDS.top.toFloat() + 105 val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) - DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, - mockWindowDecoration) + mockWindowDecoration + ) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) @@ -161,9 +251,11 @@ class DragPositioningCallbackUtilityTest { val newY = STARTING_BOUNDS.top.toFloat() + 80 val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) - DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, - mockWindowDecoration) + mockWindowDecoration + ) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top + 80) assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right - 80) @@ -180,9 +272,11 @@ class DragPositioningCallbackUtilityTest { val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) - DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, - mockWindowDecoration) + mockWindowDecoration + ) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right) @@ -193,52 +287,123 @@ class DragPositioningCallbackUtilityTest { fun testDragEndSnapsTaskBoundsWhenOutsideValidDragArea() { val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat()) val repositionTaskBounds = Rect(STARTING_BOUNDS) - val validDragArea = Rect(DISPLAY_BOUNDS.left - 100, + val validDragArea = Rect( + DISPLAY_BOUNDS.left - 100, STABLE_BOUNDS.top, DISPLAY_BOUNDS.right - 100, - DISPLAY_BOUNDS.bottom - 100) + DISPLAY_BOUNDS.bottom - 100 + ) - DragPositioningCallbackUtility.updateTaskBounds(repositionTaskBounds, STARTING_BOUNDS, - startingPoint, startingPoint.x - 1000, (DISPLAY_BOUNDS.bottom + 1000).toFloat()) - DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(repositionTaskBounds, - validDragArea) + DragPositioningCallbackUtility.updateTaskBounds( + repositionTaskBounds, STARTING_BOUNDS, + startingPoint, startingPoint.x - 1000, (DISPLAY_BOUNDS.bottom + 1000).toFloat() + ) + DragPositioningCallbackUtility.snapTaskBoundsIfNecessary( + repositionTaskBounds, + validDragArea + ) assertThat(repositionTaskBounds.left).isEqualTo(validDragArea.left) assertThat(repositionTaskBounds.top).isEqualTo(validDragArea.bottom) assertThat(repositionTaskBounds.right) - .isEqualTo(validDragArea.left + STARTING_BOUNDS.width()) + .isEqualTo(validDragArea.left + STARTING_BOUNDS.width()) assertThat(repositionTaskBounds.bottom) - .isEqualTo(validDragArea.bottom + STARTING_BOUNDS.height()) + .isEqualTo(validDragArea.bottom + STARTING_BOUNDS.height()) } @Test fun testChangeBounds_toDisallowedBounds_freezesAtLimit() { - val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), - STARTING_BOUNDS.bottom.toFloat()) + val startingPoint = PointF( + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.bottom.toFloat() + ) val repositionTaskBounds = Rect(STARTING_BOUNDS) // Initial resize to width and height 110px. var newX = STARTING_BOUNDS.right.toFloat() + 10 var newY = STARTING_BOUNDS.bottom.toFloat() + 10 var delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) - assertTrue(DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, - repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, - mockWindowDecoration)) + assertTrue( + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration + ) + ) // Resize width to 120px, height to disallowed area which should not result in a change. newX += 10 newY = DISALLOWED_RESIZE_AREA.top.toFloat() delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) - assertTrue(DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, - repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, - mockWindowDecoration)) + assertTrue( + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration + ) + ) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right + 20) - assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom + 10) + assertThat(repositionTaskBounds.bottom).isEqualTo(STABLE_BOUNDS.bottom) + } + + + @Test + fun testChangeBounds_beyondStableBounds_freezesAtStableBounds() { + val startingPoint = PointF( + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.bottom.toFloat() + ) + val repositionTaskBounds = Rect(STARTING_BOUNDS) + + // Resize to beyond stable bounds. + val newX = STARTING_BOUNDS.right.toFloat() + STABLE_BOUNDS.width() + val newY = STARTING_BOUNDS.bottom.toFloat() + STABLE_BOUNDS.height() + + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + assertTrue( + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration + ) + ) + assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(STABLE_BOUNDS.right) + assertThat(repositionTaskBounds.bottom).isEqualTo(STABLE_BOUNDS.bottom) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING) + fun testChangeBounds_unresizeableApp_beyondStableBounds_resetToStartingBounds() { + mockWindowDecoration.mTaskInfo.isResizeable = false + val startingPoint = PointF( + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.bottom.toFloat() + ) + val repositionTaskBounds = Rect(STARTING_BOUNDS) + + // Resize to beyond stable bounds. + val newX = STARTING_BOUNDS.right.toFloat() + STABLE_BOUNDS.width() + val newY = STARTING_BOUNDS.bottom.toFloat() + STABLE_BOUNDS.height() + + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + assertFalse( + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, + mockWindowDecoration + ) + ) + assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right) + assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS) fun taskMinWidthHeightUndefined_changeBoundsInDesktopModeLessThanMin_shouldNotChangeBounds() { - whenever(DesktopModeStatus.canEnterDesktopMode(mockContext)).thenReturn(true) + doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(mockContext) } initializeTaskInfo(taskMinWidth = -1, taskMinHeight = -1) val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat()) @@ -249,9 +414,11 @@ class DragPositioningCallbackUtilityTest { val newY = STARTING_BOUNDS.bottom.toFloat() - 99 val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) - DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, - mockWindowDecoration) + mockWindowDecoration + ) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right) @@ -261,7 +428,7 @@ class DragPositioningCallbackUtilityTest { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS) fun taskMinWidthHeightUndefined_changeBoundsInDesktopModeAllowedSize_shouldChangeBounds() { - whenever(DesktopModeStatus.canEnterDesktopMode(mockContext)).thenReturn(true) + doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(mockContext) } initializeTaskInfo(taskMinWidth = -1, taskMinHeight = -1) val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat()) @@ -272,9 +439,11 @@ class DragPositioningCallbackUtilityTest { val newY = STARTING_BOUNDS.bottom.toFloat() - 80 val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) - DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, - mockWindowDecoration) + mockWindowDecoration + ) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right - 80) @@ -293,9 +462,11 @@ class DragPositioningCallbackUtilityTest { val newY = STARTING_BOUNDS.bottom.toFloat() - 99 val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) - DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, - mockWindowDecoration) + mockWindowDecoration + ) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right) @@ -314,15 +485,68 @@ class DragPositioningCallbackUtilityTest { val newY = STARTING_BOUNDS.bottom.toFloat() - 50 val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) - DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController, - mockWindowDecoration) + mockWindowDecoration + ) assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right - 50) assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom - 50) } + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS) + fun testChangeBounds_windowSizeExceedsStableBounds_shouldBeAllowedToChangeBounds() { + val startingPoint = + PointF( + OFF_CENTER_STARTING_BOUNDS.right.toFloat(), + OFF_CENTER_STARTING_BOUNDS.bottom.toFloat() + ) + val repositionTaskBounds = Rect(OFF_CENTER_STARTING_BOUNDS) + // Increase height and width by STABLE_BOUNDS. Subtract by 5px so that it doesn't reach + // the disallowed drag area. + val offset = 5 + val newX = STABLE_BOUNDS.right.toFloat() - offset + val newY = STABLE_BOUNDS.bottom.toFloat() - offset + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, OFF_CENTER_STARTING_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration + ) + assertThat(repositionTaskBounds.width()).isGreaterThan(STABLE_BOUNDS.right) + assertThat(repositionTaskBounds.height()).isGreaterThan(STABLE_BOUNDS.bottom) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS) + fun testChangeBoundsInDesktopMode_windowSizeExceedsStableBounds_shouldBeLimitedToDisplaySize() { + doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(mockContext) } + val startingPoint = + PointF( + OFF_CENTER_STARTING_BOUNDS.right.toFloat(), + OFF_CENTER_STARTING_BOUNDS.bottom.toFloat() + ) + val repositionTaskBounds = Rect(OFF_CENTER_STARTING_BOUNDS) + // Increase height and width by STABLE_BOUNDS. Subtract by 5px so that it doesn't reach + // the disallowed drag area. + val offset = 5 + val newX = STABLE_BOUNDS.right.toFloat() - offset + val newY = STABLE_BOUNDS.bottom.toFloat() - offset + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, OFF_CENTER_STARTING_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration + ) + assertThat(repositionTaskBounds.width()).isLessThan(STABLE_BOUNDS.right) + assertThat(repositionTaskBounds.height()).isLessThan(STABLE_BOUNDS.bottom) + } + private fun initializeTaskInfo(taskMinWidth: Int = MIN_WIDTH, taskMinHeight: Int = MIN_HEIGHT) { mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { taskId = TASK_ID @@ -347,11 +571,13 @@ class DragPositioningCallbackUtilityTest { private const val NAVBAR_HEIGHT = 50 private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600) private val STARTING_BOUNDS = Rect(0, 0, 100, 100) + private val OFF_CENTER_STARTING_BOUNDS = Rect(-100, -100, 10, 10) private val DISALLOWED_RESIZE_AREA = Rect( DISPLAY_BOUNDS.left, DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT, DISPLAY_BOUNDS.right, - DISPLAY_BOUNDS.bottom) + DISPLAY_BOUNDS.bottom + ) private val STABLE_BOUNDS = Rect( DISPLAY_BOUNDS.left, DISPLAY_BOUNDS.top, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java index 4dea5a75a0e8..57469bf8c6e2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java @@ -25,6 +25,7 @@ import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE import static com.google.common.truth.Truth.assertThat; import android.annotation.NonNull; +import android.content.Context; import android.graphics.Point; import android.graphics.Region; import android.platform.test.annotations.DisableFlags; @@ -36,6 +37,7 @@ import android.util.Size; import androidx.test.filters.SmallTest; import com.android.window.flags.Flags; +import com.android.wm.shell.ShellTestCase; import com.google.common.testing.EqualsTester; @@ -43,6 +45,9 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.Arrays; +import java.util.List; + /** * Tests for {@link DragResizeWindowGeometry}. * @@ -51,17 +56,16 @@ import org.junit.runner.RunWith; */ @SmallTest @RunWith(AndroidTestingRunner.class) -public class DragResizeWindowGeometryTests { +public class DragResizeWindowGeometryTests extends ShellTestCase { private static final Size TASK_SIZE = new Size(500, 1000); private static final int TASK_CORNER_RADIUS = 10; - private static final int EDGE_RESIZE_THICKNESS = 15; - private static final int EDGE_RESIZE_DEBUG_THICKNESS = EDGE_RESIZE_THICKNESS - + (DragResizeWindowGeometry.DEBUG ? DragResizeWindowGeometry.EDGE_DEBUG_BUFFER : 0); + private static final int EDGE_RESIZE_THICKNESS = 12; + private static final int EDGE_RESIZE_HANDLE_INSET = 4; private static final int FINE_CORNER_SIZE = EDGE_RESIZE_THICKNESS * 2 + 10; private static final int LARGE_CORNER_SIZE = FINE_CORNER_SIZE + 10; private static final DragResizeWindowGeometry GEOMETRY = new DragResizeWindowGeometry( - TASK_CORNER_RADIUS, TASK_SIZE, EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE, - LARGE_CORNER_SIZE); + TASK_CORNER_RADIUS, TASK_SIZE, EDGE_RESIZE_THICKNESS, EDGE_RESIZE_HANDLE_INSET, + FINE_CORNER_SIZE, LARGE_CORNER_SIZE); // Points in the edge resize handle. Note that coordinates start from the top left. private static final Point TOP_EDGE_POINT = new Point(TASK_SIZE.getWidth() / 2, -EDGE_RESIZE_THICKNESS / 2); @@ -71,6 +75,16 @@ public class DragResizeWindowGeometryTests { TASK_SIZE.getWidth() + EDGE_RESIZE_THICKNESS / 2, TASK_SIZE.getHeight() / 2); private static final Point BOTTOM_EDGE_POINT = new Point(TASK_SIZE.getWidth() / 2, TASK_SIZE.getHeight() + EDGE_RESIZE_THICKNESS / 2); + // Points in the inset of the task bounds still within the edge resize handle. + // Note that coordinates start from the top left. + private static final Point TOP_INSET_POINT = new Point(TASK_SIZE.getWidth() / 2, + EDGE_RESIZE_HANDLE_INSET / 2); + private static final Point LEFT_INSET_POINT = new Point(EDGE_RESIZE_HANDLE_INSET / 2, + TASK_SIZE.getHeight() / 2); + private static final Point RIGHT_INSET_POINT = new Point( + TASK_SIZE.getWidth() - EDGE_RESIZE_HANDLE_INSET / 2, TASK_SIZE.getHeight() / 2); + private static final Point BOTTOM_INSET_POINT = new Point(TASK_SIZE.getWidth() / 2, + TASK_SIZE.getHeight() - EDGE_RESIZE_HANDLE_INSET / 2); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -85,20 +99,24 @@ public class DragResizeWindowGeometryTests { .addEqualityGroup( GEOMETRY, new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, - EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE, LARGE_CORNER_SIZE)) + EDGE_RESIZE_THICKNESS, EDGE_RESIZE_HANDLE_INSET, FINE_CORNER_SIZE, + LARGE_CORNER_SIZE)) .addEqualityGroup( new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, - EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE, LARGE_CORNER_SIZE), + EDGE_RESIZE_THICKNESS + 10, EDGE_RESIZE_HANDLE_INSET, + FINE_CORNER_SIZE, LARGE_CORNER_SIZE), new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, - EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE, LARGE_CORNER_SIZE)) - .addEqualityGroup(new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, - EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE, LARGE_CORNER_SIZE + 5), + EDGE_RESIZE_THICKNESS + 10, EDGE_RESIZE_HANDLE_INSET, + FINE_CORNER_SIZE, LARGE_CORNER_SIZE)) + .addEqualityGroup( new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, - EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE, LARGE_CORNER_SIZE + 5)) - .addEqualityGroup(new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, - EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE + 4, LARGE_CORNER_SIZE), + EDGE_RESIZE_THICKNESS + 10, EDGE_RESIZE_HANDLE_INSET, + FINE_CORNER_SIZE, + LARGE_CORNER_SIZE + 5), new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, - EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE + 4, LARGE_CORNER_SIZE)) + EDGE_RESIZE_THICKNESS + 10, EDGE_RESIZE_HANDLE_INSET, + FINE_CORNER_SIZE, + LARGE_CORNER_SIZE + 5)) .testEquals(); } @@ -124,21 +142,21 @@ public class DragResizeWindowGeometryTests { private static void verifyHorizontalEdge(@NonNull Region region, @NonNull Point point) { assertThat(region.contains(point.x, point.y)).isTrue(); // Horizontally along the edge is still contained. - assertThat(region.contains(point.x + EDGE_RESIZE_DEBUG_THICKNESS, point.y)).isTrue(); - assertThat(region.contains(point.x - EDGE_RESIZE_DEBUG_THICKNESS, point.y)).isTrue(); + assertThat(region.contains(point.x + EDGE_RESIZE_THICKNESS, point.y)).isTrue(); + assertThat(region.contains(point.x - EDGE_RESIZE_THICKNESS, point.y)).isTrue(); // Vertically along the edge is not contained. - assertThat(region.contains(point.x, point.y - EDGE_RESIZE_DEBUG_THICKNESS)).isFalse(); - assertThat(region.contains(point.x, point.y + EDGE_RESIZE_DEBUG_THICKNESS)).isFalse(); + assertThat(region.contains(point.x, point.y - EDGE_RESIZE_THICKNESS)).isFalse(); + assertThat(region.contains(point.x, point.y + EDGE_RESIZE_THICKNESS + 10)).isFalse(); } private static void verifyVerticalEdge(@NonNull Region region, @NonNull Point point) { assertThat(region.contains(point.x, point.y)).isTrue(); // Horizontally along the edge is not contained. - assertThat(region.contains(point.x + EDGE_RESIZE_DEBUG_THICKNESS, point.y)).isFalse(); - assertThat(region.contains(point.x - EDGE_RESIZE_DEBUG_THICKNESS, point.y)).isFalse(); + assertThat(region.contains(point.x + EDGE_RESIZE_THICKNESS, point.y)).isFalse(); + assertThat(region.contains(point.x - EDGE_RESIZE_THICKNESS, point.y)).isFalse(); // Vertically along the edge is contained. - assertThat(region.contains(point.x, point.y - EDGE_RESIZE_DEBUG_THICKNESS)).isTrue(); - assertThat(region.contains(point.x, point.y + EDGE_RESIZE_DEBUG_THICKNESS)).isTrue(); + assertThat(region.contains(point.x, point.y - EDGE_RESIZE_THICKNESS)).isTrue(); + assertThat(region.contains(point.x, point.y + EDGE_RESIZE_THICKNESS)).isTrue(); } /** @@ -151,12 +169,9 @@ public class DragResizeWindowGeometryTests { public void testRegionUnion_edgeDragResizeEnabled_containsLargeCorners() { Region region = new Region(); GEOMETRY.union(region); - // Make sure we're choosing a point outside of any debug region buffer. - final int cornerRadius = DragResizeWindowGeometry.DEBUG - ? Math.max(LARGE_CORNER_SIZE / 2, EDGE_RESIZE_DEBUG_THICKNESS) - : LARGE_CORNER_SIZE / 2; + final int cornerRadius = LARGE_CORNER_SIZE / 2; - new TestPoints(TASK_SIZE, cornerRadius).validateRegion(region); + new TestPoints(mContext, TASK_SIZE, cornerRadius).validateRegion(region); } /** @@ -168,11 +183,9 @@ public class DragResizeWindowGeometryTests { public void testRegionUnion_edgeDragResizeDisabled_containsFineCorners() { Region region = new Region(); GEOMETRY.union(region); - final int cornerRadius = DragResizeWindowGeometry.DEBUG - ? Math.max(LARGE_CORNER_SIZE / 2, EDGE_RESIZE_DEBUG_THICKNESS) - : LARGE_CORNER_SIZE / 2; + final int cornerRadius = FINE_CORNER_SIZE / 2; - new TestPoints(TASK_SIZE, cornerRadius).validateRegion(region); + new TestPoints(mContext, TASK_SIZE, cornerRadius).validateRegion(region); } @Test @@ -194,25 +207,26 @@ public class DragResizeWindowGeometryTests { } private void validateCtrlTypeForEdges(boolean isTouchscreen, boolean isEdgeResizePermitted) { - assertThat(GEOMETRY.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, - LEFT_EDGE_POINT.x, LEFT_EDGE_POINT.y)).isEqualTo( - isEdgeResizePermitted ? CTRL_TYPE_LEFT : CTRL_TYPE_UNDEFINED); - assertThat(GEOMETRY.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, - TOP_EDGE_POINT.x, TOP_EDGE_POINT.y)).isEqualTo( - isEdgeResizePermitted ? CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); - assertThat(GEOMETRY.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, - RIGHT_EDGE_POINT.x, RIGHT_EDGE_POINT.y)).isEqualTo( - isEdgeResizePermitted ? CTRL_TYPE_RIGHT : CTRL_TYPE_UNDEFINED); - assertThat(GEOMETRY.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, - BOTTOM_EDGE_POINT.x, BOTTOM_EDGE_POINT.y)).isEqualTo( - isEdgeResizePermitted ? CTRL_TYPE_BOTTOM : CTRL_TYPE_UNDEFINED); + List<Point> points = Arrays.asList(LEFT_EDGE_POINT, TOP_EDGE_POINT, RIGHT_EDGE_POINT, + BOTTOM_EDGE_POINT, LEFT_INSET_POINT, TOP_INSET_POINT, RIGHT_INSET_POINT, + BOTTOM_INSET_POINT); + List<Integer> expectedCtrlType = Arrays.asList(CTRL_TYPE_LEFT, CTRL_TYPE_TOP, + CTRL_TYPE_RIGHT, CTRL_TYPE_BOTTOM, CTRL_TYPE_LEFT, CTRL_TYPE_TOP, CTRL_TYPE_RIGHT, + CTRL_TYPE_BOTTOM); + + for (int i = 0; i < points.size(); i++) { + assertThat(GEOMETRY.calculateCtrlType(isTouchscreen, isEdgeResizePermitted, + points.get(i).x, points.get(i).y)).isEqualTo( + isEdgeResizePermitted ? expectedCtrlType.get(i) : CTRL_TYPE_UNDEFINED); + } } @Test @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) public void testCalculateControlType_edgeDragResizeEnabled_corners() { - final TestPoints fineTestPoints = new TestPoints(TASK_SIZE, FINE_CORNER_SIZE / 2); - final TestPoints largeCornerTestPoints = new TestPoints(TASK_SIZE, LARGE_CORNER_SIZE / 2); + final TestPoints fineTestPoints = new TestPoints(mContext, TASK_SIZE, FINE_CORNER_SIZE / 2); + final TestPoints largeCornerTestPoints = + new TestPoints(mContext, TASK_SIZE, LARGE_CORNER_SIZE / 2); // When the flag is enabled, points within fine corners should pass regardless of touch or // not. Points outside fine corners should not pass when using a course input (non-touch). @@ -249,8 +263,10 @@ public class DragResizeWindowGeometryTests { @Test @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) public void testCalculateControlType_edgeDragResizeDisabled_corners() { - final TestPoints fineTestPoints = new TestPoints(TASK_SIZE, FINE_CORNER_SIZE / 2); - final TestPoints largeCornerTestPoints = new TestPoints(TASK_SIZE, LARGE_CORNER_SIZE / 2); + final TestPoints fineTestPoints = + new TestPoints(mContext, TASK_SIZE, FINE_CORNER_SIZE / 2); + final TestPoints largeCornerTestPoints = + new TestPoints(mContext, TASK_SIZE, LARGE_CORNER_SIZE / 2); // When the flag is disabled, points within fine corners should pass only from touchscreen. // Edge resize permitted (indicating the event is from a cursor/stylus) should have no @@ -292,6 +308,7 @@ public class DragResizeWindowGeometryTests { * <p>Creates points that are both just within the bounds of each corner, and just outside. */ private static final class TestPoints { + private final Context mContext; private final Point mTopLeftPoint; private final Point mTopLeftPointOutside; private final Point mTopRightPoint; @@ -301,7 +318,8 @@ public class DragResizeWindowGeometryTests { private final Point mBottomRightPoint; private final Point mBottomRightPointOutside; - TestPoints(@NonNull Size taskSize, int cornerRadius) { + TestPoints(@NonNull Context context, @NonNull Size taskSize, int cornerRadius) { + mContext = context; // Point just inside corner square is included. mTopLeftPoint = new Point(-cornerRadius + 1, -cornerRadius + 1); // Point just outside corner square is excluded. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecoratorTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecoratorTests.kt new file mode 100644 index 000000000000..ce17c1df50bc --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecoratorTests.kt @@ -0,0 +1,636 @@ +/* + * Copyright (C) 2024 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.windowdecor + +import android.app.ActivityManager +import android.graphics.PointF +import android.graphics.Rect +import android.util.MathUtils.abs +import android.util.MathUtils.max +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED +import com.android.wm.shell.windowdecor.DragPositioningCallback.CtrlType +import com.google.common.truth.Truth.assertThat +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import kotlin.math.min +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.never + +/** + * Tests for the [FixedAspectRatioTaskPositionerDecorator], written in parameterized form to check + * decorators behaviour for different variations of drag actions. + * + * Build/Install/Run: + * atest WMShellUnitTests:FixedAspectRatioTaskPositionerDecoratorTests + */ +@SmallTest +@RunWith(TestParameterInjector::class) +class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){ + @Mock + private lateinit var mockDesktopWindowDecoration: DesktopModeWindowDecoration + @Mock + private lateinit var mockTaskPositioner: VeiledResizeTaskPositioner + + private lateinit var decoratedTaskPositioner: FixedAspectRatioTaskPositionerDecorator + + @Before + fun setUp() { + mockDesktopWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { + isResizeable = false + configuration.windowConfiguration.setBounds(PORTRAIT_BOUNDS) + } + doReturn(PORTRAIT_BOUNDS).`when`(mockTaskPositioner).onDragPositioningStart( + any(), any(), any()) + doReturn(Rect()).`when`(mockTaskPositioner).onDragPositioningMove(any(), any()) + doReturn(Rect()).`when`(mockTaskPositioner).onDragPositioningEnd(any(), any()) + decoratedTaskPositioner = spy( + FixedAspectRatioTaskPositionerDecorator( + mockDesktopWindowDecoration, mockTaskPositioner) + ) + } + + @Test + fun testOnDragPositioningStart_noAdjustment( + @TestParameter testCase: ResizeableOrNotResizingTestCases + ) { + val originalX = 0f + val originalY = 0f + mockDesktopWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { + isResizeable = testCase.isResizeable + } + + decoratedTaskPositioner.onDragPositioningStart(testCase.ctrlType, originalX, originalY) + + val capturedValues = getLatestOnStartArguments() + assertThat(capturedValues.ctrlType).isEqualTo(testCase.ctrlType) + assertThat(capturedValues.x).isEqualTo(originalX) + assertThat(capturedValues.y).isEqualTo(originalY) + } + + @Test + fun testOnDragPositioningStart_cornerResize_noAdjustment( + @TestParameter testCase: CornerResizeStartTestCases + ) { + val originalX = 0f + val originalY = 0f + + decoratedTaskPositioner.onDragPositioningStart(testCase.ctrlType, originalX, originalY) + + val capturedValues = getLatestOnStartArguments() + assertThat(capturedValues.ctrlType).isEqualTo(testCase.ctrlType) + assertThat(capturedValues.x).isEqualTo(originalX) + assertThat(capturedValues.y).isEqualTo(originalY) + } + + @Test + fun testOnDragPositioningStart_edgeResize_ctrlTypeAdjusted( + @TestParameter testCase: EdgeResizeStartTestCases, @TestParameter orientation: Orientation + ) { + val startingBounds = getAndMockBounds(orientation) + val startingPoint = getEdgeStartingPoint( + testCase.ctrlType, testCase.additionalEdgeCtrlType, startingBounds) + + decoratedTaskPositioner.onDragPositioningStart( + testCase.ctrlType, startingPoint.x, startingPoint.y) + + val adjustedCtrlType = testCase.ctrlType + testCase.additionalEdgeCtrlType + val capturedValues = getLatestOnStartArguments() + assertThat(capturedValues.ctrlType).isEqualTo(adjustedCtrlType) + assertThat(capturedValues.x).isEqualTo(startingPoint.x) + assertThat(capturedValues.y).isEqualTo(startingPoint.y) + } + + @Test + fun testOnDragPositioningMove_noAdjustment( + @TestParameter testCase: ResizeableOrNotResizingTestCases + ) { + val originalX = 0f + val originalY = 0f + decoratedTaskPositioner.onDragPositioningStart(testCase.ctrlType, originalX, originalX) + mockDesktopWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { + isResizeable = testCase.isResizeable + } + + decoratedTaskPositioner.onDragPositioningMove( + originalX + SMALL_DELTA, originalY + SMALL_DELTA) + + val capturedValues = getLatestOnMoveArguments() + assertThat(capturedValues.x).isEqualTo(originalX + SMALL_DELTA) + assertThat(capturedValues.y).isEqualTo(originalY + SMALL_DELTA) + } + + @Test + fun testOnDragPositioningMove_cornerResize_invalidRegion_noResize( + @TestParameter testCase: InvalidCornerResizeTestCases, + @TestParameter orientation: Orientation + ) { + val startingBounds = getAndMockBounds(orientation) + val startingPoint = getCornerStartingPoint(testCase.ctrlType, startingBounds) + + decoratedTaskPositioner.onDragPositioningStart( + testCase.ctrlType, startingPoint.x, startingPoint.y) + + val updatedBounds = decoratedTaskPositioner.onDragPositioningMove( + startingPoint.x + testCase.dragDelta.x, + startingPoint.y + testCase.dragDelta.y) + + verify(mockTaskPositioner, never()).onDragPositioningMove(any(), any()) + assertThat(updatedBounds).isEqualTo(startingBounds) + } + + + @Test + fun testOnDragPositioningMove_cornerResize_validRegion_resizeToAdjustedCoordinates( + @TestParameter testCase: ValidCornerResizeTestCases, + @TestParameter orientation: Orientation + ) { + val startingBounds = getAndMockBounds(orientation) + val startingPoint = getCornerStartingPoint(testCase.ctrlType, startingBounds) + + decoratedTaskPositioner.onDragPositioningStart( + testCase.ctrlType, startingPoint.x, startingPoint.y) + + decoratedTaskPositioner.onDragPositioningMove( + startingPoint.x + testCase.dragDelta.x, startingPoint.y + testCase.dragDelta.y) + + val adjustedDragDelta = calculateAdjustedDelta( + testCase.ctrlType, testCase.dragDelta, orientation) + val capturedValues = getLatestOnMoveArguments() + val absChangeX = abs(capturedValues.x - startingPoint.x) + val absChangeY = abs(capturedValues.y - startingPoint.y) + val resultAspectRatio = max(absChangeX, absChangeY) / min(absChangeX, absChangeY) + assertThat(capturedValues.x).isEqualTo(startingPoint.x + adjustedDragDelta.x) + assertThat(capturedValues.y).isEqualTo(startingPoint.y + adjustedDragDelta.y) + assertThat(resultAspectRatio).isEqualTo(STARTING_ASPECT_RATIO) + } + + @Test + fun testOnDragPositioningMove_edgeResize_resizeToAdjustedCoordinates( + @TestParameter testCase: EdgeResizeTestCases, + @TestParameter orientation: Orientation + ) { + val startingBounds = getAndMockBounds(orientation) + val startingPoint = getEdgeStartingPoint( + testCase.ctrlType, testCase.additionalEdgeCtrlType, startingBounds) + + decoratedTaskPositioner.onDragPositioningStart( + testCase.ctrlType, startingPoint.x, startingPoint.y) + + decoratedTaskPositioner.onDragPositioningMove( + startingPoint.x + testCase.dragDelta.x, + startingPoint.y + testCase.dragDelta.y) + + val adjustedDragDelta = calculateAdjustedDelta( + testCase.ctrlType + testCase.additionalEdgeCtrlType, + testCase.dragDelta, + orientation) + val capturedValues = getLatestOnMoveArguments() + val absChangeX = abs(capturedValues.x - startingPoint.x) + val absChangeY = abs(capturedValues.y - startingPoint.y) + val resultAspectRatio = max(absChangeX, absChangeY) / min(absChangeX, absChangeY) + assertThat(capturedValues.x).isEqualTo(startingPoint.x + adjustedDragDelta.x) + assertThat(capturedValues.y).isEqualTo(startingPoint.y + adjustedDragDelta.y) + assertThat(resultAspectRatio).isEqualTo(STARTING_ASPECT_RATIO) + } + + @Test + fun testOnDragPositioningEnd_noAdjustment( + @TestParameter testCase: ResizeableOrNotResizingTestCases + ) { + val originalX = 0f + val originalY = 0f + decoratedTaskPositioner.onDragPositioningStart(testCase.ctrlType, originalX, originalX) + mockDesktopWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { + isResizeable = testCase.isResizeable + } + + decoratedTaskPositioner.onDragPositioningEnd( + originalX + SMALL_DELTA, originalY + SMALL_DELTA) + + val capturedValues = getLatestOnEndArguments() + assertThat(capturedValues.x).isEqualTo(originalX + SMALL_DELTA) + assertThat(capturedValues.y).isEqualTo(originalY + SMALL_DELTA) + } + + @Test + fun testOnDragPositioningEnd_cornerResize_invalidRegion_endsAtPreviousValidPoint( + @TestParameter testCase: InvalidCornerResizeTestCases, + @TestParameter orientation: Orientation + ) { + val startingBounds = getAndMockBounds(orientation) + val startingPoint = getCornerStartingPoint(testCase.ctrlType, startingBounds) + + decoratedTaskPositioner.onDragPositioningStart( + testCase.ctrlType, startingPoint.x, startingPoint.y) + + decoratedTaskPositioner.onDragPositioningEnd( + startingPoint.x + testCase.dragDelta.x, + startingPoint.y + testCase.dragDelta.y) + + val capturedValues = getLatestOnEndArguments() + assertThat(capturedValues.x).isEqualTo(startingPoint.x) + assertThat(capturedValues.y).isEqualTo(startingPoint.y) + } + + @Test + fun testOnDragPositioningEnd_cornerResize_validRegion_endAtAdjustedCoordinates( + @TestParameter testCase: ValidCornerResizeTestCases, + @TestParameter orientation: Orientation + ) { + val startingBounds = getAndMockBounds(orientation) + val startingPoint = getCornerStartingPoint(testCase.ctrlType, startingBounds) + + decoratedTaskPositioner.onDragPositioningStart( + testCase.ctrlType, startingPoint.x, startingPoint.y) + + decoratedTaskPositioner.onDragPositioningEnd( + startingPoint.x + testCase.dragDelta.x, startingPoint.y + testCase.dragDelta.y) + + val adjustedDragDelta = calculateAdjustedDelta( + testCase.ctrlType, testCase.dragDelta, orientation) + val capturedValues = getLatestOnEndArguments() + val absChangeX = abs(capturedValues.x - startingPoint.x) + val absChangeY = abs(capturedValues.y - startingPoint.y) + val resultAspectRatio = max(absChangeX, absChangeY) / min(absChangeX, absChangeY) + assertThat(capturedValues.x).isEqualTo(startingPoint.x + adjustedDragDelta.x) + assertThat(capturedValues.y).isEqualTo(startingPoint.y + adjustedDragDelta.y) + assertThat(resultAspectRatio).isEqualTo(STARTING_ASPECT_RATIO) + } + + @Test + fun testOnDragPositioningEnd_edgeResize_endAtAdjustedCoordinates( + @TestParameter testCase: EdgeResizeTestCases, + @TestParameter orientation: Orientation + ) { + val startingBounds = getAndMockBounds(orientation) + val startingPoint = getEdgeStartingPoint( + testCase.ctrlType, testCase.additionalEdgeCtrlType, startingBounds) + + decoratedTaskPositioner.onDragPositioningStart( + testCase.ctrlType, startingPoint.x, startingPoint.y) + + decoratedTaskPositioner.onDragPositioningEnd( + startingPoint.x + testCase.dragDelta.x, + startingPoint.y + testCase.dragDelta.y) + + val adjustedDragDelta = calculateAdjustedDelta( + testCase.ctrlType + testCase.additionalEdgeCtrlType, + testCase.dragDelta, + orientation) + val capturedValues = getLatestOnEndArguments() + val absChangeX = abs(capturedValues.x - startingPoint.x) + val absChangeY = abs(capturedValues.y - startingPoint.y) + val resultAspectRatio = max(absChangeX, absChangeY) / min(absChangeX, absChangeY) + assertThat(capturedValues.x).isEqualTo(startingPoint.x + adjustedDragDelta.x) + assertThat(capturedValues.y).isEqualTo(startingPoint.y + adjustedDragDelta.y) + assertThat(resultAspectRatio).isEqualTo(STARTING_ASPECT_RATIO) + } + + /** + * Returns the most recent arguments passed to the `.onPositioningStart()` of the + * [mockTaskPositioner]. + */ + private fun getLatestOnStartArguments(): CtrlCoordinateCapture { + val captorCtrlType = argumentCaptor<Int>() + val captorCoordinates = argumentCaptor<Float>() + verify(mockTaskPositioner).onDragPositioningStart( + captorCtrlType.capture(), captorCoordinates.capture(), captorCoordinates.capture()) + + return CtrlCoordinateCapture(captorCtrlType.firstValue, captorCoordinates.firstValue, + captorCoordinates.secondValue) + } + + /** + * Returns the most recent arguments passed to the `.onPositioningMove()` of the + * [mockTaskPositioner]. + */ + private fun getLatestOnMoveArguments(): PointF { + val captorCoordinates = argumentCaptor<Float>() + verify(mockTaskPositioner).onDragPositioningMove( + captorCoordinates.capture(), captorCoordinates.capture()) + + return PointF(captorCoordinates.firstValue, captorCoordinates.secondValue) + } + + /** + * Returns the most recent arguments passed to the `.onPositioningEnd()` of the + * [mockTaskPositioner]. + */ + private fun getLatestOnEndArguments(): PointF { + val captorCoordinates = argumentCaptor<Float>() + verify(mockTaskPositioner).onDragPositioningEnd( + captorCoordinates.capture(), captorCoordinates.capture()) + + return PointF(captorCoordinates.firstValue, captorCoordinates.secondValue) + } + + /** + * Mocks the app bounds to correspond with a given orientation and returns the mocked bounds. + */ + private fun getAndMockBounds(orientation: Orientation): Rect { + val mockBounds = if (orientation.isPortrait) PORTRAIT_BOUNDS else LANDSCAPE_BOUNDS + doReturn(mockBounds).`when`(mockTaskPositioner).onDragPositioningStart( + any(), any(), any()) + doReturn(mockBounds).`when`(decoratedTaskPositioner).getBounds(any()) + return mockBounds + } + + /** + * Calculates the corner point a given drag action should start from, based on the [ctrlType], + * given the [startingBounds]. + */ + private fun getCornerStartingPoint(@CtrlType ctrlType: Int, startingBounds: Rect): PointF { + return when (ctrlType) { + CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT -> + PointF(startingBounds.right.toFloat(), startingBounds.bottom.toFloat()) + + CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT -> + PointF(startingBounds.left.toFloat(), startingBounds.bottom.toFloat()) + + CTRL_TYPE_TOP + CTRL_TYPE_RIGHT -> + PointF(startingBounds.right.toFloat(), startingBounds.top.toFloat()) + // CTRL_TYPE_TOP + CTRL_TYPE_LEFT + else -> + PointF(startingBounds.left.toFloat(), startingBounds.top.toFloat()) + } + } + + /** + * Calculates the point along an edge the edge resize should start from, based on the starting + * edge ([edgeCtrlType]) and the additional edge we expect to resize ([additionalEdgeCtrlType]), + * given the [startingBounds]. + */ + private fun getEdgeStartingPoint( + @CtrlType edgeCtrlType: Int, @CtrlType additionalEdgeCtrlType: Int, startingBounds: Rect + ): PointF { + val simulatedCorner = getCornerStartingPoint( + edgeCtrlType + additionalEdgeCtrlType, startingBounds) + when (additionalEdgeCtrlType) { + CTRL_TYPE_TOP -> { + simulatedCorner.offset(0f, -SMALL_DELTA) + return simulatedCorner + } + CTRL_TYPE_BOTTOM -> { + simulatedCorner.offset(0f, SMALL_DELTA) + return simulatedCorner + } + CTRL_TYPE_LEFT -> { + simulatedCorner.offset(SMALL_DELTA, 0f) + return simulatedCorner + } + // CTRL_TYPE_RIGHT + else -> { + simulatedCorner.offset(-SMALL_DELTA, 0f) + return simulatedCorner + } + } + } + + /** + * Calculates the adjustments to the drag delta we expect for a given action and orientation. + */ + private fun calculateAdjustedDelta( + @CtrlType ctrlType: Int, delta: PointF, orientation: Orientation + ): PointF { + if ((abs(delta.x) < abs(delta.y) && delta.x != 0f) || delta.y == 0f) { + // Only respect x delta if it's less than y delta but non-zero (i.e there is a change + // in x to be applied), or if the y delta is zero (i.e there is no change in y to be + // applied). + val adjustedY = if (orientation.isPortrait) + delta.x * STARTING_ASPECT_RATIO else + delta.x / STARTING_ASPECT_RATIO + if (ctrlType.isBottomRightOrTopLeftCorner()) { + return PointF(delta.x, adjustedY) + } + return PointF(delta.x, -adjustedY) + } + // Respect y delta. + val adjustedX = if (orientation.isPortrait) + delta.y / STARTING_ASPECT_RATIO else + delta.y * STARTING_ASPECT_RATIO + if (ctrlType.isBottomRightOrTopLeftCorner()) { + return PointF(adjustedX, delta.y) + } + return PointF(-adjustedX, delta.y) + } + + private fun @receiver:CtrlType Int.isBottomRightOrTopLeftCorner(): Boolean { + return this == CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT || this == CTRL_TYPE_TOP + CTRL_TYPE_LEFT + } + + private inner class CtrlCoordinateCapture(ctrl: Int, xValue: Float, yValue: Float) { + var ctrlType = ctrl + var x = xValue + var y = yValue + } + + companion object { + private val PORTRAIT_BOUNDS = Rect(100, 100, 200, 400) + private val LANDSCAPE_BOUNDS = Rect(100, 100, 400, 200) + private val STARTING_ASPECT_RATIO = PORTRAIT_BOUNDS.height() / PORTRAIT_BOUNDS.width() + private const val LARGE_DELTA = 50f + private const val SMALL_DELTA = 30f + + enum class Orientation( + val isPortrait: Boolean + ) { + PORTRAIT (true), + LANDSCAPE (false) + } + + enum class ResizeableOrNotResizingTestCases( + val ctrlType: Int, + val isResizeable: Boolean + ) { + NotResizing (CTRL_TYPE_UNDEFINED, false), + Resizeable (CTRL_TYPE_RIGHT, true) + } + + /** + * Tests cases for the start of a corner resize. + * @param ctrlType the control type of the corner the resize is initiated on. + */ + enum class CornerResizeStartTestCases( + val ctrlType: Int + ) { + BottomRightCorner (CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT), + BottomLeftCorner (CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT), + TopRightCorner (CTRL_TYPE_TOP + CTRL_TYPE_RIGHT), + TopLeftCorner (CTRL_TYPE_TOP + CTRL_TYPE_LEFT) + } + + /** + * Tests cases for the moving and ending of a invalid corner resize. Where the compass point + * (e.g `SouthEast`) represents the direction of the drag. + * @param ctrlType the control type of the corner the resize is initiated on. + * @param dragDelta the delta of the attempted drag action, from the [ctrlType]'s + * corresponding corner point. Represented as a combination a different signed small and + * large deltas which correspond to the direction/angle of drag. + */ + enum class InvalidCornerResizeTestCases( + val ctrlType: Int, + val dragDelta: PointF + ) { + BottomRightCornerNorthEastDrag ( + CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT, + PointF(LARGE_DELTA, -LARGE_DELTA)), + BottomRightCornerSouthWestDrag ( + CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT, + PointF(-LARGE_DELTA, LARGE_DELTA)), + TopLeftCornerNorthEastDrag ( + CTRL_TYPE_TOP + CTRL_TYPE_LEFT, + PointF(LARGE_DELTA, -LARGE_DELTA)), + TopLeftCornerSouthWestDrag ( + CTRL_TYPE_TOP + CTRL_TYPE_LEFT, + PointF(-LARGE_DELTA, LARGE_DELTA)), + BottomLeftCornerSouthEastDrag ( + CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT, + PointF(LARGE_DELTA, LARGE_DELTA)), + BottomLeftCornerNorthWestDrag ( + CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT, + PointF(-LARGE_DELTA, -LARGE_DELTA)), + TopRightCornerSouthEastDrag ( + CTRL_TYPE_TOP + CTRL_TYPE_RIGHT, + PointF(LARGE_DELTA, LARGE_DELTA)), + TopRightCornerNorthWestDrag ( + CTRL_TYPE_TOP + CTRL_TYPE_RIGHT, + PointF(-LARGE_DELTA, -LARGE_DELTA)), + } + + /** + * Tests cases for the moving and ending of a valid corner resize. Where the compass point + * (e.g `SouthEast`) represents the direction of the drag, followed by the expected + * behaviour in that direction (i.e `RespectY` means the y delta will be respected whereas + * `RespectX` means the x delta will be respected). + * @param ctrlType the control type of the corner the resize is initiated on. + * @param dragDelta the delta of the attempted drag action, from the [ctrlType]'s + * corresponding corner point. Represented as a combination a different signed small and + * large deltas which correspond to the direction/angle of drag. + */ + enum class ValidCornerResizeTestCases( + val ctrlType: Int, + val dragDelta: PointF, + ) { + BottomRightCornerSouthEastDragRespectY ( + CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT, + PointF(+LARGE_DELTA, SMALL_DELTA)), + BottomRightCornerSouthEastDragRespectX ( + CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT, + PointF(SMALL_DELTA, LARGE_DELTA)), + BottomRightCornerNorthWestDragRespectY ( + CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT, + PointF(-LARGE_DELTA, -SMALL_DELTA)), + BottomRightCornerNorthWestDragRespectX ( + CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT, + PointF(-SMALL_DELTA, -LARGE_DELTA)), + TopLeftCornerSouthEastDragRespectY ( + CTRL_TYPE_TOP + CTRL_TYPE_LEFT, + PointF(LARGE_DELTA, SMALL_DELTA)), + TopLeftCornerSouthEastDragRespectX ( + CTRL_TYPE_TOP + CTRL_TYPE_LEFT, + PointF(SMALL_DELTA, LARGE_DELTA)), + TopLeftCornerNorthWestDragRespectY ( + CTRL_TYPE_TOP + CTRL_TYPE_LEFT, + PointF(-LARGE_DELTA, -SMALL_DELTA)), + TopLeftCornerNorthWestDragRespectX ( + CTRL_TYPE_TOP + CTRL_TYPE_LEFT, + PointF(-SMALL_DELTA, -LARGE_DELTA)), + BottomLeftCornerSouthWestDragRespectY ( + CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT, + PointF(-LARGE_DELTA, SMALL_DELTA)), + BottomLeftCornerSouthWestDragRespectX ( + CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT, + PointF(-SMALL_DELTA, LARGE_DELTA)), + BottomLeftCornerNorthEastDragRespectY ( + CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT, + PointF(LARGE_DELTA, -SMALL_DELTA)), + BottomLeftCornerNorthEastDragRespectX ( + CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT, + PointF(SMALL_DELTA, -LARGE_DELTA)), + TopRightCornerSouthWestDragRespectY ( + CTRL_TYPE_TOP + CTRL_TYPE_RIGHT, + PointF(-LARGE_DELTA, SMALL_DELTA)), + TopRightCornerSouthWestDragRespectX ( + CTRL_TYPE_TOP + CTRL_TYPE_RIGHT, + PointF(-SMALL_DELTA, LARGE_DELTA)), + TopRightCornerNorthEastDragRespectY ( + CTRL_TYPE_TOP + CTRL_TYPE_RIGHT, + PointF(LARGE_DELTA, -SMALL_DELTA)), + TopRightCornerNorthEastDragRespectX ( + CTRL_TYPE_TOP + CTRL_TYPE_RIGHT, + PointF(+SMALL_DELTA, -LARGE_DELTA)) + } + + /** + * Tests cases for the start of an edge resize. + * @param ctrlType the control type of the edge the resize is initiated on. + * @param additionalEdgeCtrlType the expected additional edge to be included in the ctrl + * type. + */ + enum class EdgeResizeStartTestCases( + val ctrlType: Int, + val additionalEdgeCtrlType: Int + ) { + BottomOfLeftEdgeResize (CTRL_TYPE_LEFT, CTRL_TYPE_BOTTOM), + TopOfLeftEdgeResize (CTRL_TYPE_LEFT, CTRL_TYPE_TOP), + BottomOfRightEdgeResize (CTRL_TYPE_RIGHT, CTRL_TYPE_BOTTOM), + TopOfRightEdgeResize (CTRL_TYPE_RIGHT, CTRL_TYPE_TOP), + RightOfTopEdgeResize (CTRL_TYPE_TOP, CTRL_TYPE_RIGHT), + LeftOfTopEdgeResize (CTRL_TYPE_TOP, CTRL_TYPE_LEFT), + RightOfBottomEdgeResize (CTRL_TYPE_BOTTOM, CTRL_TYPE_RIGHT), + LeftOfBottomEdgeResize (CTRL_TYPE_BOTTOM, CTRL_TYPE_LEFT) + } + + /** + * Tests cases for the moving and ending of an edge resize. + * @param ctrlType the control type of the edge the resize is initiated on. + * @param additionalEdgeCtrlType the expected additional edge to be included in the ctrl + * type. + * @param dragDelta the delta of the attempted drag action, from the [ctrlType]'s + * corresponding edge point. Represented as a combination a different signed small and + * large deltas which correspond to the direction/angle of drag. + */ + enum class EdgeResizeTestCases( + val ctrlType: Int, + val additionalEdgeCtrlType: Int, + val dragDelta: PointF + ) { + BottomOfLeftEdgeResize (CTRL_TYPE_LEFT, CTRL_TYPE_BOTTOM, PointF(-SMALL_DELTA, 0f)), + TopOfLeftEdgeResize (CTRL_TYPE_LEFT, CTRL_TYPE_TOP, PointF(-SMALL_DELTA, 0f)), + BottomOfRightEdgeResize (CTRL_TYPE_RIGHT, CTRL_TYPE_BOTTOM, PointF(SMALL_DELTA, 0f)), + TopOfRightEdgeResize (CTRL_TYPE_RIGHT, CTRL_TYPE_TOP, PointF(SMALL_DELTA, 0f)), + RightOfTopEdgeResize (CTRL_TYPE_TOP, CTRL_TYPE_RIGHT, PointF(0f, -SMALL_DELTA)), + LeftOfTopEdgeResize (CTRL_TYPE_TOP, CTRL_TYPE_LEFT, PointF(0f, -SMALL_DELTA)), + RightOfBottomEdgeResize (CTRL_TYPE_BOTTOM, CTRL_TYPE_RIGHT, PointF(0f, SMALL_DELTA)), + LeftOfBottomEdgeResize (CTRL_TYPE_BOTTOM, CTRL_TYPE_LEFT, PointF(0f, SMALL_DELTA)) + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt index 666750485ef2..7543fed4b085 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt @@ -35,7 +35,6 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt -import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.any @@ -122,6 +121,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { displayId = DISPLAY_ID configuration.windowConfiguration.setBounds(STARTING_BOUNDS) configuration.windowConfiguration.displayRotation = ROTATION_90 + isResizeable = true } `when`(mockWindowDecoration.calculateValidDragArea()).thenReturn(VALID_DRAG_AREA) mockWindowDecoration.mDisplay = mockDisplay @@ -679,6 +679,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM) val rectAfterDrag = Rect(STARTING_BOUNDS) rectAfterDrag.right += 2000 + rectAfterDrag.bottom = STABLE_BOUNDS_LANDSCAPE.bottom // First drag; we should fetch stable bounds. verify(mockDisplayLayout, Mockito.times(1)).getStableBounds(any()) verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> @@ -706,8 +707,8 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(), STARTING_BOUNDS.right.toFloat() + 2000, STARTING_BOUNDS.bottom.toFloat() + 2000, CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM) - rectAfterDrag.right -= 2000 - rectAfterDrag.bottom += 2000 + rectAfterDrag.right = STABLE_BOUNDS_PORTRAIT.right + rectAfterDrag.bottom = STARTING_BOUNDS.bottom + 2000 verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> return@argThat wct.changes.any { (token, change) -> diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt index 5582e0f46321..cabd472ec263 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt @@ -22,10 +22,10 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.graphics.Bitmap import android.graphics.Color +import android.graphics.Point import android.graphics.Rect -import android.platform.test.annotations.RequiresFlagsEnabled -import android.platform.test.flag.junit.CheckFlagsRule -import android.platform.test.flag.junit.DeviceFlagsValueProvider +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.Display @@ -33,6 +33,8 @@ import android.view.LayoutInflater import android.view.SurfaceControl import android.view.SurfaceControlViewHost import android.view.View +import android.view.WindowManager +import androidx.core.graphics.toPointF import androidx.test.filters.SmallTest import com.android.window.flags.Flags import com.android.wm.shell.R @@ -40,12 +42,13 @@ import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout -import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT -import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT -import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer +import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -55,6 +58,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.kotlin.any +import org.mockito.kotlin.mock import org.mockito.kotlin.whenever /** @@ -69,11 +73,13 @@ import org.mockito.kotlin.whenever class HandleMenuTest : ShellTestCase() { @JvmField @Rule - val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + val setFlagsRule: SetFlagsRule = SetFlagsRule() @Mock private lateinit var mockDesktopWindowDecoration: DesktopModeWindowDecoration @Mock + private lateinit var mockWindowManager: WindowManager + @Mock private lateinit var onClickListener: View.OnClickListener @Mock private lateinit var onTouchListener: View.OnTouchListener @@ -92,6 +98,8 @@ class HandleMenuTest : ShellTestCase() { private lateinit var handleMenu: HandleMenu + private val menuWidthWithElevation = MENU_WIDTH + MENU_PILL_ELEVATION + @Before fun setUp() { val mockAdditionalViewHostViewContainer = AdditionalViewHostViewContainer( @@ -100,60 +108,82 @@ class HandleMenuTest : ShellTestCase() { ) { SurfaceControl.Transaction() } - val menuView = LayoutInflater.from(context).inflate( + val menuView = LayoutInflater.from(mContext).inflate( R.layout.desktop_mode_window_decor_handle_menu, null) whenever(mockDesktopWindowDecoration.addWindow( anyInt(), any(), any(), any(), anyInt(), anyInt(), anyInt(), anyInt()) ).thenReturn(mockAdditionalViewHostViewContainer) + whenever(mockDesktopWindowDecoration.addWindow( + any<View>(), any(), any(), any(), anyInt(), anyInt(), anyInt(), anyInt()) + ).thenReturn(mockAdditionalViewHostViewContainer) whenever(mockAdditionalViewHostViewContainer.view).thenReturn(menuView) whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width()) whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height()) whenever(displayLayout.isLandscape).thenReturn(true) - mockDesktopWindowDecoration.mDecorWindowContext = context + mContext.orCreateTestableResources.apply { + addOverride(R.dimen.desktop_mode_handle_menu_width, MENU_WIDTH) + addOverride(R.dimen.desktop_mode_handle_menu_height, MENU_HEIGHT) + addOverride(R.dimen.desktop_mode_handle_menu_margin_top, MENU_TOP_MARGIN) + addOverride(R.dimen.desktop_mode_handle_menu_margin_start, MENU_START_MARGIN) + addOverride(R.dimen.desktop_mode_handle_menu_pill_elevation, MENU_PILL_ELEVATION) + addOverride( + R.dimen.desktop_mode_handle_menu_pill_spacing_margin, MENU_PILL_SPACING_MARGIN) + } + mockDesktopWindowDecoration.mDecorWindowContext = mContext } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) + @EnableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX) fun testFullscreenMenuUsesSystemViewContainer() { createTaskInfo(WINDOWING_MODE_FULLSCREEN, SPLIT_POSITION_UNDEFINED) - val handleMenu = createAndShowHandleMenu() - assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalSystemViewContainer) + val handleMenu = createAndShowHandleMenu(SPLIT_POSITION_UNDEFINED) + assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer) // Verify menu is created at coordinates that, when added to WindowManager, // show at the top-center of display. - assertTrue(handleMenu.mHandleMenuPosition.equals(16f, -512f)) + val expected = Point(DISPLAY_BOUNDS.centerX() - menuWidthWithElevation / 2, MENU_TOP_MARGIN) + assertEquals(expected.toPointF(), handleMenu.handleMenuPosition) } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) + @EnableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX) fun testFreeformMenu_usesViewHostViewContainer() { createTaskInfo(WINDOWING_MODE_FREEFORM, SPLIT_POSITION_UNDEFINED) - handleMenu = createAndShowHandleMenu() - assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalViewHostViewContainer) + handleMenu = createAndShowHandleMenu(SPLIT_POSITION_UNDEFINED) + assertTrue(handleMenu.handleMenuViewContainer is AdditionalViewHostViewContainer) // Verify menu is created near top-left of task. - assertTrue(handleMenu.mHandleMenuPosition.equals(12f, 8f)) + val expected = Point(MENU_START_MARGIN, MENU_TOP_MARGIN) + assertEquals(expected.toPointF(), handleMenu.handleMenuPosition) } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) + @EnableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX) fun testSplitLeftMenu_usesSystemViewContainer() { createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_TOP_OR_LEFT) - handleMenu = createAndShowHandleMenu() - assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalSystemViewContainer) + handleMenu = createAndShowHandleMenu(SPLIT_POSITION_TOP_OR_LEFT) + assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer) // Verify menu is created at coordinates that, when added to WindowManager, - // show at the top of split left task. - assertTrue(handleMenu.mHandleMenuPosition.equals(-624f, -512f)) + // show at the top-center of split left task. + val expected = Point( + SPLIT_LEFT_BOUNDS.centerX() - menuWidthWithElevation / 2, + MENU_TOP_MARGIN + ) + assertEquals(expected.toPointF(), handleMenu.handleMenuPosition) } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) + @EnableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX) fun testSplitRightMenu_usesSystemViewContainer() { createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_BOTTOM_OR_RIGHT) - handleMenu = createAndShowHandleMenu() - assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalSystemViewContainer) + handleMenu = createAndShowHandleMenu(SPLIT_POSITION_BOTTOM_OR_RIGHT) + assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer) // Verify menu is created at coordinates that, when added to WindowManager, - // show at the top of split right task. - assertTrue(handleMenu.mHandleMenuPosition.equals(656f, -512f)) + // show at the top-center of split right task. + val expected = Point( + SPLIT_RIGHT_BOUNDS.centerX() - menuWidthWithElevation / 2, + MENU_TOP_MARGIN + ) + assertEquals(expected.toPointF(), handleMenu.handleMenuPosition) } private fun createTaskInfo(windowingMode: Int, splitPosition: Int) { @@ -178,28 +208,39 @@ class HandleMenuTest : ShellTestCase() { .setBounds(bounds) .setVisible(true) .build() - // Calculate captionX similar to how WindowDecoration calculates it. - whenever(mockDesktopWindowDecoration.captionX).thenReturn( - (mockDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration - .bounds.width() - context.resources.getDimensionPixelSize( - R.dimen.desktop_mode_fullscreen_decor_caption_width)) / 2) whenever(splitScreenController.getSplitPosition(any())).thenReturn(splitPosition) whenever(splitScreenController.getStageBounds(any(), any())).thenAnswer { (it.arguments.first() as Rect).set(SPLIT_LEFT_BOUNDS) + (it.arguments[1] as Rect).set(SPLIT_RIGHT_BOUNDS) } } - private fun createAndShowHandleMenu(): HandleMenu { + private fun createAndShowHandleMenu(splitPosition: Int): HandleMenu { val layoutId = if (mockDesktopWindowDecoration.mTaskInfo.isFreeform) { R.layout.desktop_mode_app_header } else { - R.layout.desktop_mode_app_header + R.layout.desktop_mode_app_handle + } + val captionX = when (mockDesktopWindowDecoration.mTaskInfo.windowingMode) { + WINDOWING_MODE_FULLSCREEN -> (DISPLAY_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2) + WINDOWING_MODE_FREEFORM -> 0 + WINDOWING_MODE_MULTI_WINDOW -> { + if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) { + (SPLIT_LEFT_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2) + } else { + (SPLIT_RIGHT_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2) + } + } + else -> error("Invalid windowing mode") } - val handleMenu = HandleMenu(mockDesktopWindowDecoration, layoutId, - onClickListener, onTouchListener, appIcon, appName, displayController, - splitScreenController, true /* shouldShowWindowingPill */, - 50 /* captionHeight */ ) - handleMenu.show() + val handleMenu = HandleMenu(mockDesktopWindowDecoration, + WindowManagerWrapper(mockWindowManager), + layoutId, appIcon, appName, splitScreenController, shouldShowWindowingPill = true, + shouldShowNewWindowButton = true, shouldShowManageWindowsButton = false, + null /* openInBrowserLink */, captionWidth = HANDLE_WIDTH, captionHeight = 50, + captionX = captionX + ) + handleMenu.show(mock(), mock(), mock(), mock(), mock(), mock(), mock(), mock()) return handleMenu } @@ -208,5 +249,12 @@ class HandleMenuTest : ShellTestCase() { private val FREEFORM_BOUNDS = Rect(500, 500, 2000, 1200) private val SPLIT_LEFT_BOUNDS = Rect(0, 0, 1280, 1600) private val SPLIT_RIGHT_BOUNDS = Rect(1280, 0, 2560, 1600) + private const val MENU_WIDTH = 200 + private const val MENU_HEIGHT = 400 + private const val MENU_TOP_MARGIN = 10 + private const val MENU_START_MARGIN = 20 + private const val MENU_PILL_ELEVATION = 2 + private const val MENU_PILL_SPACING_MARGIN = 4 + private const val HANDLE_WIDTH = 80 } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/OWNERS new file mode 100644 index 000000000000..553540cbb86c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 929241 +# includes OWNERS from parent directories
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt index a07be79579eb..e0d16aab1e07 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt @@ -97,7 +97,7 @@ class ResizeVeilTest : ShellTestCase() { .thenReturn(spyResizeVeilSurfaceBuilder) doReturn(mockResizeVeilSurface).whenever(spyResizeVeilSurfaceBuilder).build() whenever(mockSurfaceControlBuilderFactory - .create(eq("Resize veil background of Task=" + taskInfo.taskId), any())) + .create(eq("Resize veil background of Task=" + taskInfo.taskId))) .thenReturn(spyBackgroundSurfaceBuilder) doReturn(mockBackgroundSurface).whenever(spyBackgroundSurfaceBuilder).build() whenever(mockSurfaceControlBuilderFactory diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt index 48ac1e5717aa..1273ee823159 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt @@ -17,9 +17,13 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager import android.app.WindowConfiguration +import android.content.Context +import android.content.res.Resources import android.graphics.Point import android.graphics.Rect +import android.os.Handler import android.os.IBinder +import android.os.Looper import android.testing.AndroidTestingRunner import android.view.Display import android.view.Surface.ROTATION_0 @@ -31,6 +35,8 @@ import android.view.WindowManager.TRANSIT_CHANGE import android.window.TransitionInfo import android.window.WindowContainerToken import androidx.test.filters.SmallTest +import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread +import com.android.internal.jank.InteractionJankMonitor import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.DisplayController @@ -98,6 +104,13 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { private lateinit var mockFinishCallback: TransitionFinishCallback @Mock private lateinit var mockTransitions: Transitions + @Mock + private lateinit var mockContext: Context + @Mock + private lateinit var mockResources: Resources + @Mock + private lateinit var mockInteractionJankMonitor: InteractionJankMonitor + private val mainHandler = Handler(Looper.getMainLooper()) private lateinit var taskPositioner: VeiledResizeTaskPositioner @@ -105,6 +118,9 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) + mockDesktopWindowDecoration.mDisplay = mockDisplay + mockDesktopWindowDecoration.mDecorWindowContext = mockContext + whenever(mockContext.getResources()).thenReturn(mockResources) whenever(taskToken.asBinder()).thenReturn(taskBinder) whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout) whenever(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI) @@ -129,6 +145,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { displayId = DISPLAY_ID configuration.windowConfiguration.setBounds(STARTING_BOUNDS) configuration.windowConfiguration.displayRotation = ROTATION_90 + isResizeable = true } `when`(mockDesktopWindowDecoration.calculateValidDragArea()).thenReturn(VALID_DRAG_AREA) mockDesktopWindowDecoration.mDisplay = mockDisplay @@ -141,12 +158,14 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { mockDisplayController, mockDragStartListener, mockTransactionFactory, - mockTransitions + mockTransitions, + mockInteractionJankMonitor, + mainHandler, ) } @Test - fun testDragResize_noMove_doesNotShowResizeVeil() { + fun testDragResize_noMove_doesNotShowResizeVeil() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, STARTING_BOUNDS.left.toFloat(), @@ -158,6 +177,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) + verify(mockTransitions, never()).startTransition(eq(TRANSIT_CHANGE), argThat { wct -> return@argThat wct.changes.any { (token, change) -> token == taskBinder && @@ -168,7 +188,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_movesTask_doesNotShowResizeVeil() { + fun testDragResize_movesTask_doesNotShowResizeVeil() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, STARTING_BOUNDS.left.toFloat(), @@ -187,7 +207,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { verify(mockTransaction).setPosition(any(), eq(rectAfterMove.left.toFloat()), eq(rectAfterMove.top.toFloat())) - taskPositioner.onDragPositioningEnd( + val endBounds = taskPositioner.onDragPositioningEnd( STARTING_BOUNDS.left.toFloat() + 70, STARTING_BOUNDS.top.toFloat() + 20 ) @@ -199,16 +219,11 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { verify(mockDesktopWindowDecoration, never()).showResizeVeil(any()) verify(mockDesktopWindowDecoration, never()).hideResizeVeil() - verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - token == taskBinder && - (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && - change.configuration.windowConfiguration.bounds == rectAfterEnd }}, - eq(taskPositioner)) + Assert.assertEquals(rectAfterEnd, endBounds) } @Test - fun testDragResize_resize_boundsUpdateOnEnd() { + fun testDragResize_resize_boundsUpdateOnEnd() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, STARTING_BOUNDS.right.toFloat(), @@ -249,7 +264,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_noEffectiveMove_skipsTransactionOnEnd() { + fun testDragResize_noEffectiveMove_skipsTransactionOnEnd() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, STARTING_BOUNDS.left.toFloat(), @@ -281,9 +296,8 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { }) } - @Test - fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() { + fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, // drag STARTING_BOUNDS.left.toFloat(), @@ -308,7 +322,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_resize_resizingTaskReorderedToTopWhenNotFocused() { + fun testDragResize_resize_resizingTaskReorderedToTopWhenNotFocused() = runOnUiThread { mockDesktopWindowDecoration.mTaskInfo.isFocused = false taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT, // Resize right @@ -324,7 +338,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_resize_resizingTaskNotReorderedToTopWhenFocused() { + fun testDragResize_resize_resizingTaskNotReorderedToTopWhenFocused() = runOnUiThread { mockDesktopWindowDecoration.mTaskInfo.isFocused = true taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT, // Resize right @@ -340,7 +354,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_drag_draggedTaskNotReorderedToTop() { + fun testDragResize_drag_draggedTaskNotReorderedToTop() = runOnUiThread { mockDesktopWindowDecoration.mTaskInfo.isFocused = false taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, // drag @@ -357,13 +371,14 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_drag_updatesStableBoundsOnRotate() { + fun testDragResize_drag_updatesStableBoundsOnRotate() = runOnUiThread { // Test landscape stable bounds performDrag(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(), STARTING_BOUNDS.right.toFloat() + 2000, STARTING_BOUNDS.bottom.toFloat() + 2000, CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM) val rectAfterDrag = Rect(STARTING_BOUNDS) rectAfterDrag.right += 2000 + rectAfterDrag.bottom = STABLE_BOUNDS_LANDSCAPE.bottom // First drag; we should fetch stable bounds. verify(mockDisplayLayout, times(1)).getStableBounds(any()) verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct -> @@ -388,8 +403,8 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { performDrag(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(), STARTING_BOUNDS.right.toFloat() + 2000, STARTING_BOUNDS.bottom.toFloat() + 2000, CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM) - rectAfterDrag.right -= 2000 - rectAfterDrag.bottom += 2000 + rectAfterDrag.right = STABLE_BOUNDS_PORTRAIT.right + rectAfterDrag.bottom = STARTING_BOUNDS.bottom + 2000 verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct -> return@argThat wct.changes.any { (token, change) -> @@ -402,7 +417,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testIsResizingOrAnimatingResizeSet() { + fun testIsResizingOrAnimatingResizeSet() = runOnUiThread { Assert.assertFalse(taskPositioner.isResizingOrAnimating) taskPositioner.onDragPositioningStart( @@ -429,7 +444,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testIsResizingOrAnimatingResizeResetAfterStartAnimation() { + fun testIsResizingOrAnimatingResizeResetAfterStartAnimation() = runOnUiThread { performDrag( STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat(), STARTING_BOUNDS.left.toFloat() - 20, STARTING_BOUNDS.top.toFloat() - 20, @@ -443,7 +458,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testStartAnimation_useEndRelOffset() { + fun testStartAnimation_useEndRelOffset() = runOnUiThread { val changeMock = mock(TransitionInfo.Change::class.java) val startTransaction = mock(Transaction::class.java) val finishTransaction = mock(Transaction::class.java) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index f3603e1d9b46..7252b32efc6b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -18,6 +18,9 @@ package com.android.wm.shell.windowdecor; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; +import static android.view.InsetsSource.FLAG_FORCE_CONSUMING; +import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR; import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsets.Type.mandatorySystemGestures; import static android.view.WindowInsets.Type.statusBars; @@ -28,6 +31,7 @@ import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceCon import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertEquals; @@ -48,16 +52,19 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.quality.Strictness.LENIENT; +import android.annotation.NonNull; import android.app.ActivityManager; import android.content.Context; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.util.DisplayMetrics; import android.view.AttachedSurfaceControl; import android.view.Display; +import android.view.InsetsSource; import android.view.InsetsState; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; @@ -75,11 +82,14 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.shared.DesktopModeStatus; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.tests.R; import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer; +import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHost; +import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -105,6 +115,9 @@ public class WindowDecorationTests extends ShellTestCase { private static final int CORNER_RADIUS = 20; private static final int STATUS_BAR_INSET_SOURCE_ID = 0; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); + private final WindowDecoration.RelayoutResult<TestView> mRelayoutResult = new WindowDecoration.RelayoutResult<>(); @@ -117,6 +130,10 @@ public class WindowDecorationTests extends ShellTestCase { @Mock private SurfaceControlViewHost mMockSurfaceControlViewHost; @Mock + private WindowDecorViewHostSupplier mMockWindowDecorViewHostSupplier; + @Mock + private WindowDecorViewHost mMockWindowDecorViewHost; + @Mock private AttachedSurfaceControl mMockRootSurfaceControl; @Mock private TestView mMockView; @@ -149,11 +166,16 @@ public class WindowDecorationTests extends ShellTestCase { mRelayoutParams.mShadowRadiusId = R.dimen.test_window_decor_shadow_radius; mRelayoutParams.mCornerRadius = CORNER_RADIUS; + when(mMockDisplayController.getDisplay(Display.DEFAULT_DISPLAY)) + .thenReturn(mock(Display.class)); doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory) .create(any(), any(), any()); when(mMockSurfaceControlViewHost.getRootSurfaceControl()) .thenReturn(mMockRootSurfaceControl); when(mMockView.findViewById(anyInt())).thenReturn(mMockView); + when(mMockWindowDecorViewHostSupplier.acquire(any(), any())) + .thenReturn(mMockWindowDecorViewHost); + when(mMockWindowDecorViewHost.getSurfaceControl()).thenReturn(mock(SurfaceControl.class)); // Add status bar inset so that WindowDecoration does not think task is in immersive mode mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, statusBars()).setVisible(true); @@ -217,10 +239,6 @@ public class WindowDecorationTests extends ShellTestCase { final SurfaceControl.Builder decorContainerSurfaceBuilder = createMockSurfaceControlBuilder(decorContainerSurface); mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder); - final SurfaceControl captionContainerSurface = mock(SurfaceControl.class); - final SurfaceControl.Builder captionContainerSurfaceBuilder = - createMockSurfaceControlBuilder(captionContainerSurface); - mMockSurfaceControlBuilders.add(captionContainerSurfaceBuilder); final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() .setDisplayId(Display.DEFAULT_DISPLAY) @@ -241,18 +259,18 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockSurfaceControlStartT).setTrustedOverlay(decorContainerSurface, true); verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 300, 100); - verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface); - verify(captionContainerSurfaceBuilder).setContainerLayer(); + final SurfaceControl captionContainerSurface = mMockWindowDecorViewHost.getSurfaceControl(); + verify(mMockSurfaceControlStartT).reparent(captionContainerSurface, decorContainerSurface); verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64); verify(mMockSurfaceControlStartT).show(captionContainerSurface); - verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any()); - - verify(mMockSurfaceControlViewHost) - .setView(same(mMockView), - argThat(lp -> lp.height == 64 - && lp.width == 300 - && (lp.flags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0)); + verify(mMockWindowDecorViewHost).updateView( + same(mMockView), + argThat(lp -> lp.height == 64 + && lp.width == 300 + && (lp.flags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0), + eq(taskInfo.configuration), + eq(null) /* onDrawTransaction */); verify(mMockView).setTaskFocusState(true); verify(mMockWindowContainerTransaction).addInsetsSource( eq(taskInfo.token), @@ -260,7 +278,8 @@ public class WindowDecorationTests extends ShellTestCase { eq(0 /* index */), eq(WindowInsets.Type.captionBar()), eq(new Rect(100, 300, 400, 364)), - any()); + any(), + anyInt()); verify(mMockSurfaceControlStartT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS); verify(mMockSurfaceControlFinishT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS); @@ -282,10 +301,6 @@ public class WindowDecorationTests extends ShellTestCase { final SurfaceControl.Builder decorContainerSurfaceBuilder = createMockSurfaceControlBuilder(decorContainerSurface); mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder); - final SurfaceControl captionContainerSurface = mock(SurfaceControl.class); - final SurfaceControl.Builder captionContainerSurfaceBuilder = - createMockSurfaceControlBuilder(captionContainerSurface); - mMockSurfaceControlBuilders.add(captionContainerSurfaceBuilder); final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); mMockSurfaceControlTransactions.add(t); @@ -308,7 +323,7 @@ public class WindowDecorationTests extends ShellTestCase { windowDecor.relayout(taskInfo); - verify(mMockSurfaceControlViewHost, never()).release(); + verify(mMockWindowDecorViewHost, never()).release(any()); verify(t, never()).apply(); verify(mMockWindowContainerTransaction, never()) .removeInsetsSource(eq(taskInfo.token), any(), anyInt(), anyInt()); @@ -318,9 +333,8 @@ public class WindowDecorationTests extends ShellTestCase { taskInfo.isVisible = false; windowDecor.relayout(taskInfo); - final InOrder releaseOrder = inOrder(t2, mMockSurfaceControlViewHost); - releaseOrder.verify(mMockSurfaceControlViewHost).release(); - releaseOrder.verify(t2).remove(captionContainerSurface); + final InOrder releaseOrder = inOrder(t2, mMockWindowDecorViewHostSupplier); + releaseOrder.verify(mMockWindowDecorViewHostSupplier).release(mMockWindowDecorViewHost, t2); releaseOrder.verify(t2).remove(decorContainerSurface); releaseOrder.verify(t2).apply(); // Expect to remove two insets sources, the caption insets and the mandatory gesture insets. @@ -368,8 +382,8 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockDisplayController).removeDisplayWindowListener(same(listener)); assertThat(mRelayoutResult.mRootView).isSameInstanceAs(mMockView); - verify(mMockSurfaceControlViewHostFactory).create(any(), eq(mockDisplay), any()); - verify(mMockSurfaceControlViewHost).setView(same(mMockView), any()); + verify(mMockWindowDecorViewHostSupplier).acquire(any(), eq(mockDisplay)); + verify(mMockWindowDecorViewHost).updateView(same(mMockView), any(), any(), any()); } @Test @@ -382,10 +396,6 @@ public class WindowDecorationTests extends ShellTestCase { final SurfaceControl.Builder decorContainerSurfaceBuilder = createMockSurfaceControlBuilder(decorContainerSurface); mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder); - final SurfaceControl captionContainerSurface = mock(SurfaceControl.class); - final SurfaceControl.Builder captionContainerSurfaceBuilder = - createMockSurfaceControlBuilder(captionContainerSurface); - mMockSurfaceControlBuilders.add(captionContainerSurfaceBuilder); final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); mMockSurfaceControlTransactions.add(t); @@ -422,8 +432,7 @@ public class WindowDecorationTests extends ShellTestCase { windowDecor.mDecorWindowContext.getResources(), mRelayoutParams.mCaptionHeightId); verify(mMockSurfaceControlAddWindowT).setWindowCrop(additionalWindowSurface, width, height); verify(mMockSurfaceControlAddWindowT).show(additionalWindowSurface); - verify(mMockSurfaceControlViewHostFactory, Mockito.times(2)) - .create(any(), eq(defaultDisplay), any()); + verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any()); } @Test @@ -436,10 +445,6 @@ public class WindowDecorationTests extends ShellTestCase { final SurfaceControl.Builder decorContainerSurfaceBuilder = createMockSurfaceControlBuilder(decorContainerSurface); mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder); - final SurfaceControl captionContainerSurface = mock(SurfaceControl.class); - final SurfaceControl.Builder captionContainerSurfaceBuilder = - createMockSurfaceControlBuilder(captionContainerSurface); - mMockSurfaceControlBuilders.add(captionContainerSurfaceBuilder); final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); mMockSurfaceControlTransactions.add(t); @@ -459,8 +464,8 @@ public class WindowDecorationTests extends ShellTestCase { windowDecor.relayout(taskInfo); - verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface); - verify(captionContainerSurfaceBuilder).setContainerLayer(); + final SurfaceControl captionContainerSurface = mMockWindowDecorViewHost.getSurfaceControl(); + verify(mMockSurfaceControlStartT).reparent(captionContainerSurface, decorContainerSurface); // Width of the captionContainerSurface should match the width of TASK_BOUNDS verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64); verify(mMockSurfaceControlStartT).show(captionContainerSurface); @@ -476,10 +481,6 @@ public class WindowDecorationTests extends ShellTestCase { final SurfaceControl.Builder decorContainerSurfaceBuilder = createMockSurfaceControlBuilder(decorContainerSurface); mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder); - final SurfaceControl captionContainerSurface = mock(SurfaceControl.class); - final SurfaceControl.Builder captionContainerSurfaceBuilder = - createMockSurfaceControlBuilder(captionContainerSurface); - mMockSurfaceControlBuilders.add(captionContainerSurfaceBuilder); final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); mMockSurfaceControlTransactions.add(t); @@ -497,9 +498,11 @@ public class WindowDecorationTests extends ShellTestCase { taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); - windowDecor.relayout(taskInfo, true /* applyStartTransactionOnDraw */); + mRelayoutParams.mApplyStartTransactionOnDraw = true; + windowDecor.relayout(taskInfo); - verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockSurfaceControlStartT); + verify(mMockWindowDecorViewHost).updateView(any(), any(), any(), + eq(mMockSurfaceControlStartT)); } @Test @@ -565,9 +568,9 @@ public class WindowDecorationTests extends ShellTestCase { windowDecor.relayout(taskInfo); verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(), - eq(0) /* index */, eq(captionBar()), any(), any()); + eq(0) /* index */, eq(captionBar()), any(), any(), anyInt()); verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(), - eq(0) /* index */, eq(mandatorySystemGestures()), any(), any()); + eq(0) /* index */, eq(mandatorySystemGestures()), any(), any(), anyInt()); } @Test @@ -619,15 +622,15 @@ public class WindowDecorationTests extends ShellTestCase { .setVisible(true) .setBounds(new Rect(0, 0, 1000, 1000)) .build(); + taskInfo.isFocused = true; + // Caption visible at first. + when(mMockDisplayController.getInsetsState(taskInfo.displayId)) + .thenReturn(createInsetsState(statusBars(), true /* visible */)); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); - - // Run it once so that insets are added. - mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(true); windowDecor.relayout(taskInfo); - // Run it again so that insets are removed. - mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(false); - windowDecor.relayout(taskInfo); + // Hide caption so insets are removed. + windowDecor.onInsetsStateChanged(createInsetsState(statusBars(), false /* visible */)); verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(), eq(0) /* index */, eq(captionBar())); @@ -646,17 +649,17 @@ public class WindowDecorationTests extends ShellTestCase { .setVisible(true) .setBounds(new Rect(0, 0, 1000, 1000)) .build(); - final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); - // Hidden from the beginning, so no insets were ever added. - mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(false); + when(mMockDisplayController.getInsetsState(taskInfo.displayId)) + .thenReturn(createInsetsState(statusBars(), false /* visible */)); + final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); windowDecor.relayout(taskInfo); // Never added. verify(mMockWindowContainerTransaction, never()).addInsetsSource(eq(taskInfo.token), any(), - eq(0) /* index */, eq(captionBar()), any(), any()); + eq(0) /* index */, eq(captionBar()), any(), any(), anyInt()); verify(mMockWindowContainerTransaction, never()).addInsetsSource(eq(taskInfo.token), any(), - eq(0) /* index */, eq(mandatorySystemGestures()), any(), any()); + eq(0) /* index */, eq(mandatorySystemGestures()), any(), any(), anyInt()); // No need to remove them if they were never added. verify(mMockWindowContainerTransaction, never()).removeInsetsSource(eq(taskInfo.token), any(), eq(0) /* index */, eq(captionBar())); @@ -681,9 +684,9 @@ public class WindowDecorationTests extends ShellTestCase { mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(true); windowDecor.relayout(taskInfo); verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(), - eq(0) /* index */, eq(captionBar()), any(), any()); + eq(0) /* index */, eq(captionBar()), any(), any(), anyInt()); verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(), - eq(0) /* index */, eq(mandatorySystemGestures()), any(), any()); + eq(0) /* index */, eq(mandatorySystemGestures()), any(), any(), anyInt()); windowDecor.close(); @@ -738,9 +741,9 @@ public class WindowDecorationTests extends ShellTestCase { // Insets should be applied twice. verify(mMockWindowContainerTransaction, times(2)).addInsetsSource(eq(token), any(), - eq(0) /* index */, eq(captionBar()), any(), any()); + eq(0) /* index */, eq(captionBar()), any(), any(), anyInt()); verify(mMockWindowContainerTransaction, times(2)).addInsetsSource(eq(token), any(), - eq(0) /* index */, eq(mandatorySystemGestures()), any(), any()); + eq(0) /* index */, eq(mandatorySystemGestures()), any(), any(), anyInt()); } @Test @@ -765,9 +768,32 @@ public class WindowDecorationTests extends ShellTestCase { // Insets should only need to be applied once. verify(mMockWindowContainerTransaction, times(1)).addInsetsSource(eq(token), any(), - eq(0) /* index */, eq(captionBar()), any(), any()); + eq(0) /* index */, eq(captionBar()), any(), any(), anyInt()); verify(mMockWindowContainerTransaction, times(1)).addInsetsSource(eq(token), any(), - eq(0) /* index */, eq(mandatorySystemGestures()), any(), any()); + eq(0) /* index */, eq(mandatorySystemGestures()), any(), any(), anyInt()); + } + + @Test + public void testRelayout_captionInsetSourceFlags() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + final WindowContainerToken token = TestRunningTaskInfoBuilder.createMockWCToken(); + final TestRunningTaskInfoBuilder builder = new TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setVisible(true); + + final ActivityManager.RunningTaskInfo taskInfo = + builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build(); + final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); + mRelayoutParams.mInsetSourceFlags = + FLAG_FORCE_CONSUMING | FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR; + windowDecor.relayout(taskInfo); + + // Caption inset source should add params' flags. + verify(mMockWindowContainerTransaction).addInsetsSource(eq(token), any(), + eq(0) /* index */, eq(captionBar()), any(), any(), + eq(FLAG_FORCE_CONSUMING | FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR)); } @Test @@ -830,45 +856,153 @@ public class WindowDecorationTests extends ShellTestCase { } @Test - public void updateViewHost_applyTransactionOnDrawIsTrue_surfaceControlIsUpdated() { + public void relayout_applyTransactionOnDrawIsTrue_updatesViewWithDrawTransaction() { final TestWindowDecoration windowDecor = createWindowDecoration( - new TestRunningTaskInfoBuilder().build()); + new TestRunningTaskInfoBuilder() + .setVisible(true) + .setWindowingMode(WINDOWING_MODE_FREEFORM) + .build()); mRelayoutParams.mApplyStartTransactionOnDraw = true; + mRelayoutResult.mRootView = mMockView; - windowDecor.updateViewHost(mRelayoutParams, mMockSurfaceControlStartT, mRelayoutResult); + windowDecor.relayout(windowDecor.mTaskInfo); - verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockSurfaceControlStartT); + verify(mMockWindowDecorViewHost) + .updateView(eq(mRelayoutResult.mRootView), any(), + eq(windowDecor.mTaskInfo.configuration), eq(mMockSurfaceControlStartT)); } @Test - public void updateViewHost_nullDrawTransaction_applyTransactionOnDrawIsTrue_throwsException() { + public void relayout_applyTransactionOnDrawIsTrue_asyncViewHostRendering_throwsException() { final TestWindowDecoration windowDecor = createWindowDecoration( - new TestRunningTaskInfoBuilder().build()); + new TestRunningTaskInfoBuilder() + .setVisible(true) + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .build()); mRelayoutParams.mApplyStartTransactionOnDraw = true; + mRelayoutParams.mAsyncViewHost = true; + mRelayoutResult.mRootView = mMockView; assertThrows(IllegalArgumentException.class, - () -> windowDecor.updateViewHost( - mRelayoutParams, null /* onDrawTransaction */, mRelayoutResult)); + () -> windowDecor.relayout(windowDecor.mTaskInfo)); } @Test - public void updateViewHost_nullDrawTransaction_applyTransactionOnDrawIsFalse_doesNotThrow() { + public void relayout_asyncViewHostRendering() { final TestWindowDecoration windowDecor = createWindowDecoration( - new TestRunningTaskInfoBuilder().build()); - mRelayoutParams.mApplyStartTransactionOnDraw = false; + new TestRunningTaskInfoBuilder() + .setVisible(true) + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .build()); + mRelayoutParams.mAsyncViewHost = true; + mRelayoutResult.mRootView = mMockView; + + windowDecor.relayout(windowDecor.mTaskInfo); + + verify(mMockWindowDecorViewHost) + .updateViewAsync(eq(mRelayoutResult.mRootView), any(), + eq(windowDecor.mTaskInfo.configuration)); + } + + @Test + public void onStatusBarVisibilityChange_fullscreen_shownToHidden_hidesCaption() { + final ActivityManager.RunningTaskInfo task = createTaskInfo(); + task.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + when(mMockDisplayController.getInsetsState(task.displayId)) + .thenReturn(createInsetsState(statusBars(), true /* visible */)); + final TestWindowDecoration decor = createWindowDecoration(task); + decor.relayout(task); + assertTrue(decor.mIsCaptionVisible); + + decor.onInsetsStateChanged(createInsetsState(statusBars(), false /* visible */)); + + assertFalse(decor.mIsCaptionVisible); + } - windowDecor.updateViewHost(mRelayoutParams, null /* onDrawTransaction */, mRelayoutResult); + @Test + public void onStatusBarVisibilityChange_fullscreen_hiddenToShown_showsCaption() { + final ActivityManager.RunningTaskInfo task = createTaskInfo(); + task.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + when(mMockDisplayController.getInsetsState(task.displayId)) + .thenReturn(createInsetsState(statusBars(), false /* visible */)); + final TestWindowDecoration decor = createWindowDecoration(task); + decor.relayout(task); + assertFalse(decor.mIsCaptionVisible); + + decor.onInsetsStateChanged(createInsetsState(statusBars(), true /* visible */)); + + assertTrue(decor.mIsCaptionVisible); + } + + @Test + public void onStatusBarVisibilityChange_freeform_shownToHidden_keepsCaption() { + final ActivityManager.RunningTaskInfo task = createTaskInfo(); + task.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + when(mMockDisplayController.getInsetsState(task.displayId)) + .thenReturn(createInsetsState(statusBars(), true /* visible */)); + final TestWindowDecoration decor = createWindowDecoration(task); + decor.relayout(task); + assertTrue(decor.mIsCaptionVisible); + + decor.onInsetsStateChanged(createInsetsState(statusBars(), false /* visible */)); + + assertTrue(decor.mIsCaptionVisible); + } + + @Test + public void onKeyguardStateChange_hiddenToShownAndOccluding_hidesCaption() { + final ActivityManager.RunningTaskInfo task = createTaskInfo(); + when(mMockDisplayController.getInsetsState(task.displayId)) + .thenReturn(createInsetsState(statusBars(), true /* visible */)); + final TestWindowDecoration decor = createWindowDecoration(task); + decor.relayout(task); + assertTrue(decor.mIsCaptionVisible); + + decor.onKeyguardStateChanged(true /* visible */, true /* occluding */); + + assertFalse(decor.mIsCaptionVisible); + } + + @Test + public void onKeyguardStateChange_showingAndOccludingToHidden_showsCaption() { + final ActivityManager.RunningTaskInfo task = createTaskInfo(); + when(mMockDisplayController.getInsetsState(task.displayId)) + .thenReturn(createInsetsState(statusBars(), true /* visible */)); + final TestWindowDecoration decor = createWindowDecoration(task); + decor.onKeyguardStateChanged(true /* visible */, true /* occluding */); + assertFalse(decor.mIsCaptionVisible); + + decor.onKeyguardStateChanged(false /* visible */, false /* occluding */); + + assertTrue(decor.mIsCaptionVisible); + } + + private ActivityManager.RunningTaskInfo createTaskInfo() { + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setVisible(true) + .build(); + taskInfo.isFocused = true; + return taskInfo; + } + + private InsetsState createInsetsState(@WindowInsets.Type.InsetsType int type, boolean visible) { + final InsetsState state = new InsetsState(); + final InsetsSource source = new InsetsSource(0, type); + source.setVisible(visible); + state.addSource(source); + return state; } private TestWindowDecoration createWindowDecoration(ActivityManager.RunningTaskInfo taskInfo) { - return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer, - taskInfo, mMockTaskSurface, + return new TestWindowDecoration(mContext, mContext, mMockDisplayController, + mMockShellTaskOrganizer, taskInfo, mMockTaskSurface, new MockObjectSupplier<>(mMockSurfaceControlBuilders, () -> createMockSurfaceControlBuilder(mock(SurfaceControl.class))), new MockObjectSupplier<>(mMockSurfaceControlTransactions, () -> mock(SurfaceControl.Transaction.class)), () -> mMockWindowContainerTransaction, () -> mMockTaskSurface, - mMockSurfaceControlViewHostFactory); + mMockSurfaceControlViewHostFactory, + mMockWindowDecorViewHostSupplier); } private class MockObjectSupplier<T> implements Supplier<T> { @@ -900,23 +1034,28 @@ public class WindowDecorationTests extends ShellTestCase { } private class TestWindowDecoration extends WindowDecoration<TestView> { - TestWindowDecoration(Context context, DisplayController displayController, + TestWindowDecoration(Context context, @NonNull Context userContext, + DisplayController displayController, ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, Supplier<WindowContainerTransaction> windowContainerTransactionSupplier, Supplier<SurfaceControl> surfaceControlSupplier, - SurfaceControlViewHostFactory surfaceControlViewHostFactory) { - super(context, displayController, taskOrganizer, taskInfo, taskSurface, + SurfaceControlViewHostFactory surfaceControlViewHostFactory, + @NonNull WindowDecorViewHostSupplier windowDecorViewHostSupplier) { + super(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface, surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, windowContainerTransactionSupplier, surfaceControlSupplier, - surfaceControlViewHostFactory); + surfaceControlViewHostFactory, windowDecorViewHostSupplier); } @Override void relayout(ActivityManager.RunningTaskInfo taskInfo) { - relayout(taskInfo, false /* applyStartTransactionOnDraw */); + mRelayoutParams.mRunningTaskInfo = taskInfo; + mRelayoutParams.mLayoutResId = R.layout.caption_layout; + relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT, + mMockWindowContainerTransaction, mMockView, mRelayoutResult); } @Override @@ -924,12 +1063,17 @@ public class WindowDecorationTests extends ShellTestCase { return null; } - void relayout(ActivityManager.RunningTaskInfo taskInfo, - boolean applyStartTransactionOnDraw) { - mRelayoutParams.mRunningTaskInfo = taskInfo; - mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; - relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT, - mMockWindowContainerTransaction, mMockView, mRelayoutResult); + @Override + int getCaptionViewId() { + return R.id.caption; + } + + @Override + TestView inflateLayout(Context context, int layoutResId) { + if (layoutResId == R.layout.caption_layout) { + return mMockView; + } + return super.inflateLayout(context, layoutResId); } private AdditionalViewContainer addTestViewContainer() { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt index d3e996b12e1f..0f52ed7f1c02 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt @@ -25,11 +25,12 @@ import android.view.WindowManager import androidx.test.filters.SmallTest import com.android.wm.shell.R import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.windowdecor.WindowManagerWrapper import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.kotlin.any +import org.mockito.kotlin.argThat import org.mockito.kotlin.eq import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -66,16 +67,25 @@ class AdditionalSystemViewContainerTest : ShellTestCase() { @Test fun testReleaseView_ViewRemoved() { + val flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH viewContainer = AdditionalSystemViewContainer( mockContext, - R.layout.desktop_mode_window_decor_handle_menu, + WindowManagerWrapper(mockWindowManager), TASK_ID, X, Y, WIDTH, - HEIGHT + HEIGHT, + flags, + R.layout.desktop_mode_window_decor_handle_menu + ) + verify(mockWindowManager).addView( + eq(mockView), + argThat { + lp -> (lp as WindowManager.LayoutParams).flags == flags + } ) - verify(mockWindowManager).addView(eq(mockView), any()) viewContainer.releaseView() verify(mockWindowManager).removeViewImmediate(mockView) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt new file mode 100644 index 000000000000..1b2ce9e4df36 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2024 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.windowdecor.viewhost + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.SurfaceControl +import android.view.SurfaceControlViewHost +import android.view.View +import android.view.WindowManager +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertThrows +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify + + +/** + * Tests for [DefaultWindowDecorViewHost]. + * + * Build/Install/Run: + * atest WMShellUnitTests:DefaultWindowDecorViewHostTest + */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner::class) +class DefaultWindowDecorViewHostTest : ShellTestCase() { + + @Test + fun updateView_layoutInViewHost() = runTest { + val windowDecorViewHost = createDefaultViewHost() + val view = View(context) + + windowDecorViewHost.updateView( + view = view, + attrs = WindowManager.LayoutParams(100, 100), + configuration = context.resources.configuration, + onDrawTransaction = null + ) + + assertThat(windowDecorViewHost.viewHost).isNotNull() + assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(view) + } + + @Test + fun updateView_alreadyLaidOut_relayouts() = runTest { + val windowDecorViewHost = createDefaultViewHost() + val view = View(context) + windowDecorViewHost.updateView( + view = view, + attrs = WindowManager.LayoutParams(100, 100), + configuration = context.resources.configuration, + onDrawTransaction = null + ) + + val otherParams = WindowManager.LayoutParams(200, 200) + windowDecorViewHost.updateView( + view = view, + attrs = otherParams, + configuration = context.resources.configuration, + onDrawTransaction = null + ) + + assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(view) + assertThat(windowDecorViewHost.viewHost!!.view!!.layoutParams.width) + .isEqualTo(otherParams.width) + } + + @Test + fun updateView_replacingView_throws() = runTest { + val windowDecorViewHost = createDefaultViewHost() + val view = View(context) + windowDecorViewHost.updateView( + view = view, + attrs = WindowManager.LayoutParams(100, 100), + configuration = context.resources.configuration, + onDrawTransaction = null + ) + + val otherView = View(context) + assertThrows(Exception::class.java) { + windowDecorViewHost.updateView( + view = otherView, + attrs = WindowManager.LayoutParams(100, 100), + configuration = context.resources.configuration, + onDrawTransaction = null + ) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun updateView_clearsPendingAsyncJob() = runTest { + val windowDecorViewHost = createDefaultViewHost() + val asyncView = View(context) + val syncView = View(context) + val asyncAttrs = WindowManager.LayoutParams(100, 100) + val syncAttrs = WindowManager.LayoutParams(200, 200) + + windowDecorViewHost.updateViewAsync( + view = asyncView, + attrs = asyncAttrs, + configuration = context.resources.configuration, + ) + + // No view host yet, since the coroutine hasn't run. + assertThat(windowDecorViewHost.viewHost).isNull() + + windowDecorViewHost.updateView( + view = syncView, + attrs = syncAttrs, + configuration = context.resources.configuration, + onDrawTransaction = null + ) + + // Would run coroutine if it hadn't been cancelled. + advanceUntilIdle() + + assertThat(windowDecorViewHost.viewHost).isNotNull() + assertThat(windowDecorViewHost.viewHost!!.view).isNotNull() + // View host view/attrs should match the ones from the sync call, plus, since the + // sync/async were made with different views, if the job hadn't been cancelled there + // would've been an exception thrown as replacing views isn't allowed. + assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(syncView) + assertThat(windowDecorViewHost.viewHost!!.view!!.layoutParams.width) + .isEqualTo(syncAttrs.width) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun updateViewAsync() = runTest { + val windowDecorViewHost = createDefaultViewHost() + val view = View(context) + val attrs = WindowManager.LayoutParams(100, 100) + + windowDecorViewHost.updateViewAsync( + view = view, + attrs = attrs, + configuration = context.resources.configuration, + ) + + assertThat(windowDecorViewHost.viewHost).isNull() + + advanceUntilIdle() + + assertThat(windowDecorViewHost.viewHost).isNotNull() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun updateViewAsync_clearsPendingAsyncJob() = runTest { + val windowDecorViewHost = createDefaultViewHost() + + val view = View(context) + windowDecorViewHost.updateViewAsync( + view = view, + attrs = WindowManager.LayoutParams(100, 100), + configuration = context.resources.configuration, + ) + val otherView = View(context) + windowDecorViewHost.updateViewAsync( + view = otherView, + attrs = WindowManager.LayoutParams(100, 100), + configuration = context.resources.configuration, + ) + + advanceUntilIdle() + + assertThat(windowDecorViewHost.viewHost).isNotNull() + assertThat(windowDecorViewHost.viewHost!!.view).isNotNull() + assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(otherView) + } + + @Test + fun release() = runTest { + val windowDecorViewHost = createDefaultViewHost() + + val view = View(context) + windowDecorViewHost.updateView( + view = view, + attrs = WindowManager.LayoutParams(100, 100), + configuration = context.resources.configuration, + onDrawTransaction = null + ) + + val t = mock(SurfaceControl.Transaction::class.java) + windowDecorViewHost.release(t) + + verify(windowDecorViewHost.viewHost!!).release() + verify(t).remove(windowDecorViewHost.surfaceControl) + } + + private fun CoroutineScope.createDefaultViewHost() = DefaultWindowDecorViewHost( + context = context, + mainScope = this, + display = context.display, + surfaceControlViewHostFactory = { c, d, wwm, s -> + spy(SurfaceControlViewHost(c, d, wwm, s)) + } + ) +}
\ No newline at end of file diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index 15ef58ecf3bd..1bc15d72bacc 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -44,6 +44,9 @@ cc_defaults { "-Werror", "-Wunreachable-code", ], + header_libs: [ + "native_headers", + ], target: { windows: { // The Windows compiler warns incorrectly for value initialization with {}. @@ -212,6 +215,7 @@ cc_test { "tests/AttributeResolution_test.cpp", "tests/BigBuffer_test.cpp", "tests/ByteBucketArray_test.cpp", + "tests/CombinedIterator_test.cpp", "tests/Config_test.cpp", "tests/ConfigDescription_test.cpp", "tests/ConfigLocale_test.cpp", @@ -269,6 +273,7 @@ cc_test { cc_benchmark { name: "libandroidfw_benchmarks", defaults: ["libandroidfw_defaults"], + test_config: "tests/AndroidTest_Benchmarks.xml", srcs: [ // Helpers/infra for benchmarking. "tests/BenchMain.cpp", @@ -284,7 +289,11 @@ cc_benchmark { "tests/Theme_bench.cpp", ], shared_libs: common_test_libs, - data: ["tests/data/**/*.apk"], + data: [ + "tests/data/**/*.apk", + ":FrameworkResourcesSparseTestApp", + ":FrameworkResourcesNotSparseTestApp", + ], } cc_library { diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index 46f636e2ae7f..0fa31c7a832e 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -23,9 +23,11 @@ #include <map> #include <set> #include <span> +#include <utility> #include "android-base/logging.h" #include "android-base/stringprintf.h" +#include "androidfw/CombinedIterator.h" #include "androidfw/ResourceTypes.h" #include "androidfw/ResourceUtils.h" #include "androidfw/Util.h" @@ -105,7 +107,7 @@ AssetManager2::AssetManager2(ApkAssetsList apk_assets, const ResTable_config& co } AssetManager2::AssetManager2() { - configurations_.resize(1); + configurations_.emplace_back(); } bool AssetManager2::SetApkAssets(ApkAssetsList apk_assets, bool invalidate_caches) { @@ -436,8 +438,8 @@ bool AssetManager2::ContainsAllocatedTable() const { return false; } -void AssetManager2::SetConfigurations(std::vector<ResTable_config> configurations, - bool force_refresh) { +void AssetManager2::SetConfigurations(std::span<const ResTable_config> configurations, + bool force_refresh) { int diff = 0; if (force_refresh) { diff = -1; @@ -450,8 +452,10 @@ void AssetManager2::SetConfigurations(std::vector<ResTable_config> configuration } } } - configurations_ = std::move(configurations); - + configurations_.clear(); + for (auto&& config : configurations) { + configurations_.emplace_back(config); + } if (diff) { RebuildFilterList(); InvalidateCaches(static_cast<uint32_t>(diff)); @@ -1622,6 +1626,12 @@ Theme::Theme(AssetManager2* asset_manager) : asset_manager_(asset_manager) { Theme::~Theme() = default; +static bool IsUndefined(const Res_value& value) { + // DATA_NULL_EMPTY (@empty) is a valid resource value and DATA_NULL_UNDEFINED represents + // an absence of a valid value. + return value.dataType == Res_value::TYPE_NULL && value.data != Res_value::DATA_NULL_EMPTY; +} + base::expected<std::monostate, NullOrIOError> Theme::ApplyStyle(uint32_t resid, bool force) { ATRACE_NAME("Theme::ApplyStyle"); @@ -1633,39 +1643,76 @@ base::expected<std::monostate, NullOrIOError> Theme::ApplyStyle(uint32_t resid, // Merge the flags from this style. type_spec_flags_ |= (*bag)->type_spec_flags; + // + // This function is the most expensive part of applying an frro to the existing app resources, + // and needs to be as efficient as possible. + // The data structure we're working with is two parallel sorted arrays of keys (resource IDs) + // and entries (resource value + some attributes). + // The styles get applied in sequence, starting with an empty set of attributes. Each style + // contains its values for the theme attributes, and gets applied in either normal or forced way: + // - normal way never overrides the existing attribute, so only unique style attributes are added + // - forced way overrides anything for that attribute, and if it's undefined it removes the + // previous value completely + // + // Style attributes come in a Bag data type - a sorted array of attributes with their values. This + // means we don't need to re-sort the attributes ever, and instead: + // - for an already existing attribute just skip it or apply the forced value + // - if the forced value is undefined, mark it undefined as well to get rid of it later + // - for a new attribute append it to the array, forming a new sorted section of new attributes + // past the end of the original ones (ignore undefined ones here) + // - inplace merge two sorted sections to form a single sorted array again. + // - run the last pass to remove all undefined elements + // + // Using this algorithm performs better than a repeated binary search + insert in the middle, + // as that keeps shifting the tail end of the arrays and wasting CPU cycles in memcpy(). + // + const auto starting_size = keys_.size(); + if (starting_size == 0) { + keys_.reserve((*bag)->entry_count); + entries_.reserve((*bag)->entry_count); + } + bool wrote_undefined = false; for (auto it = begin(*bag); it != end(*bag); ++it) { const uint32_t attr_res_id = it->key; - // If the resource ID passed in is not a style, the key can be some other identifier that is not // a resource ID. We should fail fast instead of operating with strange resource IDs. if (!is_valid_resid(attr_res_id)) { return base::unexpected(std::nullopt); } - - // DATA_NULL_EMPTY (@empty) is a valid resource value and DATA_NULL_UNDEFINED represents - // an absence of a valid value. - bool is_undefined = it->value.dataType == Res_value::TYPE_NULL && - it->value.data != Res_value::DATA_NULL_EMPTY; + const bool is_undefined = IsUndefined(it->value); if (!force && is_undefined) { continue; } - - const auto key_it = std::lower_bound(keys_.begin(), keys_.end(), attr_res_id); - const auto entry_it = entries_.begin() + (key_it - keys_.begin()); - if (key_it != keys_.end() && *key_it == attr_res_id) { - if (is_undefined) { - // DATA_NULL_UNDEFINED clears the value of the attribute in the theme only when `force` is - // true. - keys_.erase(key_it); - entries_.erase(entry_it); - } else if (force) { + const auto key_it = std::lower_bound(keys_.begin(), keys_.begin() + starting_size, attr_res_id); + if (key_it != keys_.begin() + starting_size && *key_it == attr_res_id) { + const auto entry_it = entries_.begin() + (key_it - keys_.begin()); + if (force || IsUndefined(entry_it->value)) { *entry_it = Entry{it->cookie, (*bag)->type_spec_flags, it->value}; + wrote_undefined |= is_undefined; } - } else { - keys_.insert(key_it, attr_res_id); - entries_.insert(entry_it, Entry{it->cookie, (*bag)->type_spec_flags, it->value}); + } else if (!is_undefined) { + keys_.emplace_back(attr_res_id); + entries_.emplace_back(it->cookie, (*bag)->type_spec_flags, it->value); } } + + if (starting_size && keys_.size() != starting_size) { + std::inplace_merge( + CombinedIterator(keys_.begin(), entries_.begin()), + CombinedIterator(keys_.begin() + starting_size, entries_.begin() + starting_size), + CombinedIterator(keys_.end(), entries_.end())); + } + if (wrote_undefined) { + auto new_end = std::remove_if(CombinedIterator(keys_.begin(), entries_.begin()), + CombinedIterator(keys_.end(), entries_.end()), + [](const auto& pair) { return IsUndefined(pair.second.value); }); + keys_.erase(new_end.it1, keys_.end()); + entries_.erase(new_end.it2, entries_.end()); + } + if (android::base::kEnableDChecks && !std::is_sorted(keys_.begin(), keys_.end())) { + ALOGW("Bag %u was unsorted in the apk?", unsigned(resid)); + return base::unexpected(std::nullopt); + } return {}; } @@ -1691,6 +1738,9 @@ std::optional<AssetManager2::SelectedValue> Theme::GetAttribute(uint32_t resid) return std::nullopt; } const auto entry_it = entries_.begin() + (key_it - keys_.begin()); + if (IsUndefined(entry_it->value)) { + return std::nullopt; + } type_spec_flags |= entry_it->type_spec_flags; if (entry_it->value.dataType == Res_value::TYPE_ATTRIBUTE) { resid = entry_it->value.data; diff --git a/libs/androidfw/BigBuffer.cpp b/libs/androidfw/BigBuffer.cpp index bedfc49a1b0d..43b56c32fb79 100644 --- a/libs/androidfw/BigBuffer.cpp +++ b/libs/androidfw/BigBuffer.cpp @@ -17,8 +17,8 @@ #include <androidfw/BigBuffer.h> #include <algorithm> +#include <iterator> #include <memory> -#include <vector> #include "android-base/logging.h" @@ -78,10 +78,27 @@ void* BigBuffer::NextBlock(size_t* out_size) { std::string BigBuffer::to_string() const { std::string result; + result.reserve(size_); for (const Block& block : blocks_) { result.append(block.buffer.get(), block.buffer.get() + block.size); } return result; } +void BigBuffer::AppendBuffer(BigBuffer&& buffer) { + std::move(buffer.blocks_.begin(), buffer.blocks_.end(), std::back_inserter(blocks_)); + size_ += buffer.size_; + buffer.blocks_.clear(); + buffer.size_ = 0; +} + +void BigBuffer::BackUp(size_t count) { + Block& block = blocks_.back(); + block.size -= count; + size_ -= count; + // BigBuffer is supposed to always give zeroed memory, but backing up usually means + // something has been already written into the block. Erase it. + std::fill_n(block.buffer.get() + block.size, count, 0); +} + } // namespace android diff --git a/libs/androidfw/Idmap.cpp b/libs/androidfw/Idmap.cpp index 982419059ead..f066e4620675 100644 --- a/libs/androidfw/Idmap.cpp +++ b/libs/androidfw/Idmap.cpp @@ -121,7 +121,7 @@ OverlayDynamicRefTable::OverlayDynamicRefTable(const Idmap_data_header* data_hea uint8_t target_assigned_package_id) : data_header_(data_header), entries_(entries), - target_assigned_package_id_(target_assigned_package_id) { }; + target_assigned_package_id_(target_assigned_package_id) {} status_t OverlayDynamicRefTable::lookupResourceId(uint32_t* resId) const { const Idmap_overlay_entry* first_entry = entries_; diff --git a/libs/androidfw/StringPool.cpp b/libs/androidfw/StringPool.cpp index 1cb8df311c89..629f14683b19 100644 --- a/libs/androidfw/StringPool.cpp +++ b/libs/androidfw/StringPool.cpp @@ -132,7 +132,7 @@ bool StringPool::StyleRef::operator==(const StyleRef& rhs) const { auto rhs_iter = rhs.entry_->spans.begin(); for (const Span& span : entry_->spans) { - const Span& rhs_span = *rhs_iter; + const Span& rhs_span = *rhs_iter++; if (span.first_char != rhs_span.first_char || span.last_char != rhs_span.last_char || span.name != rhs_span.name) { return false; @@ -297,24 +297,22 @@ void StringPool::Prune() { template <typename E> static void SortEntries( std::vector<std::unique_ptr<E>>& entries, - const std::function<int(const StringPool::Context&, const StringPool::Context&)>& cmp) { + base::function_ref<int(const StringPool::Context&, const StringPool::Context&)> cmp) { using UEntry = std::unique_ptr<E>; + std::sort(entries.begin(), entries.end(), [cmp](const UEntry& a, const UEntry& b) -> bool { + int r = cmp(a->context, b->context); + if (r == 0) { + r = a->value.compare(b->value); + } + return r < 0; + }); +} - if (cmp != nullptr) { - std::sort(entries.begin(), entries.end(), [&cmp](const UEntry& a, const UEntry& b) -> bool { - int r = cmp(a->context, b->context); - if (r == 0) { - r = a->value.compare(b->value); - } - return r < 0; - }); - } else { - std::sort(entries.begin(), entries.end(), - [](const UEntry& a, const UEntry& b) -> bool { return a->value < b->value; }); - } +void StringPool::Sort() { + Sort([](auto&&, auto&&) { return 0; }); } -void StringPool::Sort(const std::function<int(const Context&, const Context&)>& cmp) { +void StringPool::Sort(base::function_ref<int(const Context&, const Context&)> cmp) { SortEntries(styles_, cmp); SortEntries(strings_, cmp); ReAssignIndices(); diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h index 17a8ba6c03bd..0fdeefa09e26 100644 --- a/libs/androidfw/include/androidfw/AssetManager2.h +++ b/libs/androidfw/include/androidfw/AssetManager2.h @@ -32,6 +32,7 @@ #include "androidfw/AssetManager.h" #include "androidfw/ResourceTypes.h" #include "androidfw/Util.h" +#include "ftl/small_vector.h" namespace android { @@ -159,9 +160,10 @@ class AssetManager2 { // Sets/resets the configuration for this AssetManager. This will cause all // caches that are related to the configuration change to be invalidated. - void SetConfigurations(std::vector<ResTable_config> configurations, bool force_refresh = false); + void SetConfigurations(std::span<const ResTable_config> configurations, + bool force_refresh = false); - inline const std::vector<ResTable_config>& GetConfigurations() const { + std::span<const ResTable_config> GetConfigurations() const { return configurations_; } @@ -280,9 +282,9 @@ class AssetManager2 { private: SelectedValue(uint8_t value_type, Res_value::data_type value_data, ApkAssetsCookie cookie, - uint32_t type_flags, uint32_t resid, const ResTable_config& config) : + uint32_t type_flags, uint32_t resid, ResTable_config config) : cookie(cookie), data(value_data), type(value_type), flags(type_flags), - resid(resid), config(config) {}; + resid(resid), config(std::move(config)) {} }; // Retrieves the best matching resource value with ID `resid`. @@ -470,13 +472,13 @@ class AssetManager2 { // An array mapping package ID to index into package_groups. This keeps the lookup fast // without taking too much memory. - std::array<uint8_t, std::numeric_limits<uint8_t>::max() + 1> package_ids_; + std::array<uint8_t, std::numeric_limits<uint8_t>::max() + 1> package_ids_ = {}; - uint32_t default_locale_; + uint32_t default_locale_ = 0; // The current configurations set for this AssetManager. When this changes, cached resources // may need to be purged. - std::vector<ResTable_config> configurations_; + ftl::SmallVector<ResTable_config, 1> configurations_; // Cached set of bags. These are cached because they can inherit keys from parent bags, // which involves some calculation. diff --git a/libs/androidfw/include/androidfw/BigBuffer.h b/libs/androidfw/include/androidfw/BigBuffer.h index b99a4edf9d88..c4cd7c576542 100644 --- a/libs/androidfw/include/androidfw/BigBuffer.h +++ b/libs/androidfw/include/androidfw/BigBuffer.h @@ -14,13 +14,12 @@ * limitations under the License. */ -#ifndef _ANDROID_BIG_BUFFER_H -#define _ANDROID_BIG_BUFFER_H +#pragma once -#include <cstring> #include <memory> #include <string> #include <type_traits> +#include <utility> #include <vector> #include "android-base/logging.h" @@ -150,24 +149,11 @@ inline size_t BigBuffer::block_size() const { template <typename T> inline T* BigBuffer::NextBlock(size_t count) { - static_assert(std::is_standard_layout<T>::value, "T must be standard_layout type"); + static_assert(std::is_standard_layout_v<T>, "T must be standard_layout type"); CHECK(count != 0); return reinterpret_cast<T*>(NextBlockImpl(sizeof(T) * count)); } -inline void BigBuffer::BackUp(size_t count) { - Block& block = blocks_.back(); - block.size -= count; - size_ -= count; -} - -inline void BigBuffer::AppendBuffer(BigBuffer&& buffer) { - std::move(buffer.blocks_.begin(), buffer.blocks_.end(), std::back_inserter(blocks_)); - size_ += buffer.size_; - buffer.blocks_.clear(); - buffer.size_ = 0; -} - inline void BigBuffer::Pad(size_t bytes) { NextBlock<char>(bytes); } @@ -188,5 +174,3 @@ inline BigBuffer::const_iterator BigBuffer::end() const { } } // namespace android - -#endif // _ANDROID_BIG_BUFFER_H diff --git a/libs/androidfw/include/androidfw/CombinedIterator.h b/libs/androidfw/include/androidfw/CombinedIterator.h new file mode 100644 index 000000000000..4ff6a7d7e6c9 --- /dev/null +++ b/libs/androidfw/include/androidfw/CombinedIterator.h @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <compare> +#include <iterator> +#include <utility> + +namespace android { + +namespace detail { +// A few useful aliases to not repeat them everywhere +template <class It1, class It2> +using Value = std::pair<typename std::iterator_traits<It1>::value_type, + typename std::iterator_traits<It2>::value_type>; + +template <class It1, class It2> +using BaseRefPair = std::pair<typename std::iterator_traits<It1>::reference, + typename std::iterator_traits<It2>::reference>; + +template <class It1, class It2> +struct RefPair : BaseRefPair<It1, It2> { + using Base = BaseRefPair<It1, It2>; + using Value = detail::Value<It1, It2>; + + RefPair(It1 it1, It2 it2) : Base(*it1, *it2) { + } + + RefPair& operator=(const Value& v) { + this->first = v.first; + this->second = v.second; + return *this; + } + operator Value() const { + return Value(this->first, this->second); + } + bool operator==(const RefPair& other) { + return this->first == other.first; + } + bool operator==(const Value& other) { + return this->first == other.first; + } + std::strong_ordering operator<=>(const RefPair& other) const { + return this->first <=> other.first; + } + std::strong_ordering operator<=>(const Value& other) const { + return this->first <=> other.first; + } + friend void swap(RefPair& l, RefPair& r) { + using std::swap; + swap(l.first, r.first); + swap(l.second, r.second); + } +}; + +template <class It1, class It2> +struct RefPairPtr { + RefPair<It1, It2> value; + + RefPair<It1, It2>* operator->() const { + return &value; + } +}; +} // namespace detail + +// +// CombinedIterator - a class to combine two iterators to process them as a single iterator to a +// pair of values. Useful for processing a data structure of "struct of arrays", replacing +// array of structs for cache locality. +// +// The value type is a pair of copies of the values of each iterator, and the reference is a +// pair of references to the corresponding values. Comparison only compares the first element, +// making it most useful for using on data like (vector<Key>, vector<Value>) for binary searching, +// sorting both together and so on. +// +// The class is designed for handling arrays, so it requires random access iterators as an input. +// + +template <class It1, class It2> +requires std::random_access_iterator<It1> && std::random_access_iterator<It2> +struct CombinedIterator { + typedef detail::Value<It1, It2> value_type; + typedef detail::RefPair<It1, It2> reference; + typedef std::ptrdiff_t difference_type; + typedef detail::RefPairPtr<It1, It2> pointer; + typedef std::random_access_iterator_tag iterator_category; + + CombinedIterator(It1 it1 = {}, It2 it2 = {}) : it1(it1), it2(it2) { + } + + bool operator<(const CombinedIterator& other) const { + return it1 < other.it1; + } + bool operator<=(const CombinedIterator& other) const { + return it1 <= other.it1; + } + bool operator>(const CombinedIterator& other) const { + return it1 > other.it1; + } + bool operator>=(const CombinedIterator& other) const { + return it1 >= other.it1; + } + bool operator==(const CombinedIterator& other) const { + return it1 == other.it1; + } + pointer operator->() const { + return pointer{{it1, it2}}; + } + reference operator*() const { + return {it1, it2}; + } + reference operator[](difference_type n) const { + return {it1 + n, it2 + n}; + } + + CombinedIterator& operator++() { + ++it1; + ++it2; + return *this; + } + CombinedIterator operator++(int) { + const auto res = *this; + ++*this; + return res; + } + CombinedIterator& operator--() { + --it1; + --it2; + return *this; + } + CombinedIterator operator--(int) { + const auto res = *this; + --*this; + return res; + } + CombinedIterator& operator+=(difference_type n) { + it1 += n; + it2 += n; + return *this; + } + CombinedIterator operator+(difference_type n) const { + CombinedIterator res = *this; + return res += n; + } + + CombinedIterator& operator-=(difference_type n) { + it1 -= n; + it2 -= n; + return *this; + } + CombinedIterator operator-(difference_type n) const { + CombinedIterator res = *this; + return res -= n; + } + difference_type operator-(const CombinedIterator& other) { + return it1 - other.it1; + } + + It1 it1; + It2 it2; +}; + +} // namespace android diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h index c32a38ee9ec2..64b1f0c6ed03 100644 --- a/libs/androidfw/include/androidfw/Idmap.h +++ b/libs/androidfw/include/androidfw/Idmap.h @@ -171,14 +171,14 @@ class LoadedIdmap { } // Returns a mapping from target resource ids to overlay values. - const IdmapResMap GetTargetResourcesMap(uint8_t target_assigned_package_id, - const OverlayDynamicRefTable* overlay_ref_table) const { + IdmapResMap GetTargetResourcesMap(uint8_t target_assigned_package_id, + const OverlayDynamicRefTable* overlay_ref_table) const { return IdmapResMap(data_header_, target_entries_, target_inline_entries_, inline_entry_values_, configurations_, target_assigned_package_id, overlay_ref_table); } // Returns a dynamic reference table for a loaded overlay package. - const OverlayDynamicRefTable GetOverlayDynamicRefTable(uint8_t target_assigned_package_id) const { + OverlayDynamicRefTable GetOverlayDynamicRefTable(uint8_t target_assigned_package_id) const { return OverlayDynamicRefTable(data_header_, overlay_entries_, target_assigned_package_id); } diff --git a/libs/androidfw/include/androidfw/StringPool.h b/libs/androidfw/include/androidfw/StringPool.h index 0190ab57bf23..9b2c72a29f48 100644 --- a/libs/androidfw/include/androidfw/StringPool.h +++ b/libs/androidfw/include/androidfw/StringPool.h @@ -17,7 +17,6 @@ #ifndef _ANDROID_STRING_POOL_H #define _ANDROID_STRING_POOL_H -#include <functional> #include <memory> #include <string> #include <unordered_map> @@ -25,6 +24,7 @@ #include "BigBuffer.h" #include "IDiagnostics.h" +#include "android-base/function_ref.h" #include "android-base/macros.h" #include "androidfw/ConfigDescription.h" #include "androidfw/StringPiece.h" @@ -205,7 +205,8 @@ class StringPool { // Sorts the strings according to their Context using some comparison function. // Equal Contexts are further sorted by string value, lexicographically. // If no comparison function is provided, values are only sorted lexicographically. - void Sort(const std::function<int(const Context&, const Context&)>& cmp = nullptr); + void Sort(); + void Sort(base::function_ref<int(const Context&, const Context&)> cmp); // Removes any strings that have no references. void Prune(); diff --git a/libs/androidfw/tests/AndroidTest_Benchmarks.xml b/libs/androidfw/tests/AndroidTest_Benchmarks.xml new file mode 100644 index 000000000000..e61e46fb7785 --- /dev/null +++ b/libs/androidfw/tests/AndroidTest_Benchmarks.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> +<configuration description="Runs libandroidfw_benchmarks and libandroidfw_tests."> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-native-metric" /> + + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> + <option name="cleanup" value="true" /> + <option name="push" value="libandroidfw_benchmarks->/data/local/tmp/libandroidfw_benchmarks" /> + </target_preparer> + <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" > + <option name="native-benchmark-device-path" value="/data/local/tmp" /> + <option name="benchmark-module-name" value="libandroidfw_benchmarks" /> + <!-- The GoogleBenchmarkTest class ordinarily expects every file in the benchmark's + directory (recursively) to be a google-benchmark binary, so we need this setting to + avoid failing on the test data files. --> + <option name="file-exclusion-filter-regex" value=".*\.(apk|config)$" /> + </test> +</configuration>
\ No newline at end of file diff --git a/libs/androidfw/tests/AssetManager2_bench.cpp b/libs/androidfw/tests/AssetManager2_bench.cpp index 2caa98c35971..136f5ea639a1 100644 --- a/libs/androidfw/tests/AssetManager2_bench.cpp +++ b/libs/androidfw/tests/AssetManager2_bench.cpp @@ -37,7 +37,7 @@ constexpr const static char* kFrameworkPath = "/system/framework/framework-res.a static void BM_AssetManagerLoadAssets(benchmark::State& state) { std::string path = GetTestDataPath() + "/basic/basic.apk"; - while (state.KeepRunning()) { + for (auto&& _ : state) { auto apk = ApkAssets::Load(path); AssetManager2 assets; assets.SetApkAssets({apk}); @@ -47,7 +47,7 @@ BENCHMARK(BM_AssetManagerLoadAssets); static void BM_AssetManagerLoadAssetsOld(benchmark::State& state) { String8 path((GetTestDataPath() + "/basic/basic.apk").data()); - while (state.KeepRunning()) { + for (auto&& _ : state) { AssetManager assets; assets.addAssetPath(path, nullptr /* cookie */, false /* appAsLib */, false /* isSystemAsset */); @@ -60,7 +60,7 @@ BENCHMARK(BM_AssetManagerLoadAssetsOld); static void BM_AssetManagerLoadFrameworkAssets(benchmark::State& state) { std::string path = kFrameworkPath; - while (state.KeepRunning()) { + for (auto&& _ : state) { auto apk = ApkAssets::Load(path); AssetManager2 assets; assets.SetApkAssets({apk}); @@ -70,7 +70,7 @@ BENCHMARK(BM_AssetManagerLoadFrameworkAssets); static void BM_AssetManagerLoadFrameworkAssetsOld(benchmark::State& state) { String8 path(kFrameworkPath); - while (state.KeepRunning()) { + for (auto&& _ : state) { AssetManager assets; assets.addAssetPath(path, nullptr /* cookie */, false /* appAsLib */, false /* isSystemAsset */); @@ -138,7 +138,7 @@ static void BM_AssetManagerGetBag(benchmark::State& state) { AssetManager2 assets; assets.SetApkAssets({apk}); - while (state.KeepRunning()) { + for (auto&& _ : state) { auto bag = assets.GetBag(app::R::style::StyleTwo); if (!bag.has_value()) { state.SkipWithError("Failed to load get bag"); @@ -165,7 +165,7 @@ static void BM_AssetManagerGetBagOld(benchmark::State& state) { const ResTable& table = assets.getResources(true); - while (state.KeepRunning()) { + for (auto&& _ : state) { const ResTable::bag_entry* bag_begin; const ssize_t N = table.lockBag(app::R::style::StyleTwo, &bag_begin); const ResTable::bag_entry* const bag_end = bag_begin + N; @@ -190,7 +190,7 @@ static void BM_AssetManagerGetResourceLocales(benchmark::State& state) { AssetManager2 assets; assets.SetApkAssets({apk}); - while (state.KeepRunning()) { + for (auto&& _ : state) { std::set<std::string> locales = assets.GetResourceLocales(false /*exclude_system*/, true /*merge_equivalent_languages*/); benchmark::DoNotOptimize(locales); @@ -208,7 +208,7 @@ static void BM_AssetManagerGetResourceLocalesOld(benchmark::State& state) { const ResTable& table = assets.getResources(true); - while (state.KeepRunning()) { + for (auto&& _ : state) { Vector<String8> locales; table.getLocales(&locales, true /*includeSystemLocales*/, true /*mergeEquivalentLangs*/); benchmark::DoNotOptimize(locales); @@ -231,7 +231,7 @@ static void BM_AssetManagerSetConfigurationFramework(benchmark::State& state) { std::vector<ResTable_config> configs; configs.push_back(config); - while (state.KeepRunning()) { + for (auto&& _ : state) { configs[0].sdkVersion = ~configs[0].sdkVersion; assets.SetConfigurations(configs); } @@ -251,7 +251,7 @@ static void BM_AssetManagerSetConfigurationFrameworkOld(benchmark::State& state) ResTable_config config; memset(&config, 0, sizeof(config)); - while (state.KeepRunning()) { + for (auto&& _ : state) { config.sdkVersion = ~config.sdkVersion; assets.setConfiguration(config); } diff --git a/libs/androidfw/tests/AssetManager2_test.cpp b/libs/androidfw/tests/AssetManager2_test.cpp index c62f095e9dac..3f228841f6ba 100644 --- a/libs/androidfw/tests/AssetManager2_test.cpp +++ b/libs/androidfw/tests/AssetManager2_test.cpp @@ -113,7 +113,7 @@ TEST_F(AssetManager2Test, FindsResourceFromSingleApkAssets) { desired_config.language[1] = 'e'; AssetManager2 assetmanager; - assetmanager.SetConfigurations({desired_config}); + assetmanager.SetConfigurations({{desired_config}}); assetmanager.SetApkAssets({basic_assets_}); auto value = assetmanager.GetResource(basic::R::string::test1); @@ -137,7 +137,7 @@ TEST_F(AssetManager2Test, FindsResourceFromMultipleApkAssets) { desired_config.language[1] = 'e'; AssetManager2 assetmanager; - assetmanager.SetConfigurations({desired_config}); + assetmanager.SetConfigurations({{desired_config}}); assetmanager.SetApkAssets({basic_assets_, basic_de_fr_assets_}); auto value = assetmanager.GetResource(basic::R::string::test1); @@ -466,10 +466,10 @@ TEST_F(AssetManager2Test, ResolveDeepIdReference) { TEST_F(AssetManager2Test, DensityOverride) { AssetManager2 assetmanager; assetmanager.SetApkAssets({basic_assets_, basic_xhdpi_assets_, basic_xxhdpi_assets_}); - assetmanager.SetConfigurations({{ + assetmanager.SetConfigurations({{{ .density = ResTable_config::DENSITY_XHIGH, .sdkVersion = 21, - }}); + }}}); auto value = assetmanager.GetResource(basic::R::string::density, false /*may_be_bag*/); ASSERT_TRUE(value.has_value()); @@ -721,7 +721,7 @@ TEST_F(AssetManager2Test, GetLastPathWithoutEnablingReturnsEmpty) { ResTable_config desired_config; AssetManager2 assetmanager; - assetmanager.SetConfigurations({desired_config}); + assetmanager.SetConfigurations({{desired_config}}); assetmanager.SetApkAssets({basic_assets_}); assetmanager.SetResourceResolutionLoggingEnabled(false); @@ -736,7 +736,7 @@ TEST_F(AssetManager2Test, GetLastPathWithoutResolutionReturnsEmpty) { ResTable_config desired_config; AssetManager2 assetmanager; - assetmanager.SetConfigurations({desired_config}); + assetmanager.SetConfigurations({{desired_config}}); assetmanager.SetApkAssets({basic_assets_}); auto result = assetmanager.GetLastResourceResolution(); @@ -751,7 +751,7 @@ TEST_F(AssetManager2Test, GetLastPathWithSingleApkAssets) { AssetManager2 assetmanager; assetmanager.SetResourceResolutionLoggingEnabled(true); - assetmanager.SetConfigurations({desired_config}); + assetmanager.SetConfigurations({{desired_config}}); assetmanager.SetApkAssets({basic_assets_}); auto value = assetmanager.GetResource(basic::R::string::test1); @@ -774,7 +774,7 @@ TEST_F(AssetManager2Test, GetLastPathWithMultipleApkAssets) { AssetManager2 assetmanager; assetmanager.SetResourceResolutionLoggingEnabled(true); - assetmanager.SetConfigurations({desired_config}); + assetmanager.SetConfigurations({{desired_config}}); assetmanager.SetApkAssets({basic_assets_, basic_de_fr_assets_}); auto value = assetmanager.GetResource(basic::R::string::test1); @@ -796,7 +796,7 @@ TEST_F(AssetManager2Test, GetLastPathAfterDisablingReturnsEmpty) { AssetManager2 assetmanager; assetmanager.SetResourceResolutionLoggingEnabled(true); - assetmanager.SetConfigurations({desired_config}); + assetmanager.SetConfigurations({{desired_config}}); assetmanager.SetApkAssets({basic_assets_}); auto value = assetmanager.GetResource(basic::R::string::test1); @@ -817,7 +817,7 @@ TEST_F(AssetManager2Test, GetOverlayablesToString) { AssetManager2 assetmanager; assetmanager.SetResourceResolutionLoggingEnabled(true); - assetmanager.SetConfigurations({desired_config}); + assetmanager.SetConfigurations({{desired_config}}); assetmanager.SetApkAssets({overlayable_assets_}); const auto map = assetmanager.GetOverlayableMapForPackage(0x7f); diff --git a/libs/androidfw/tests/BenchmarkHelpers.cpp b/libs/androidfw/tests/BenchmarkHelpers.cpp index 8b883f4ed1df..ec2abb84a5d5 100644 --- a/libs/androidfw/tests/BenchmarkHelpers.cpp +++ b/libs/androidfw/tests/BenchmarkHelpers.cpp @@ -28,7 +28,7 @@ void GetResourceBenchmarkOld(const std::vector<std::string>& paths, const ResTab for (const std::string& path : paths) { if (!assetmanager.addAssetPath(String8(path.c_str()), nullptr /* cookie */, false /* appAsLib */, false /* isSystemAssets */)) { - state.SkipWithError(base::StringPrintf("Failed to load assets %s", path.c_str()).c_str()); + state.SkipWithError(base::StringPrintf("Failed to old-load assets %s", path.c_str()).c_str()); return; } } @@ -57,7 +57,7 @@ void GetResourceBenchmark(const std::vector<std::string>& paths, const ResTable_ for (const std::string& path : paths) { auto apk = ApkAssets::Load(path); if (apk == nullptr) { - state.SkipWithError(base::StringPrintf("Failed to load assets %s", path.c_str()).c_str()); + state.SkipWithError(base::StringPrintf("Failed to new-load assets %s", path.c_str()).c_str()); return; } apk_assets.push_back(std::move(apk)); @@ -66,7 +66,7 @@ void GetResourceBenchmark(const std::vector<std::string>& paths, const ResTable_ AssetManager2 assetmanager; assetmanager.SetApkAssets(apk_assets); if (config != nullptr) { - assetmanager.SetConfigurations({*config}); + assetmanager.SetConfigurations({{{*config}}}); } while (state.KeepRunning()) { diff --git a/libs/androidfw/tests/BigBuffer_test.cpp b/libs/androidfw/tests/BigBuffer_test.cpp index 382d21e20846..7e38f1758057 100644 --- a/libs/androidfw/tests/BigBuffer_test.cpp +++ b/libs/androidfw/tests/BigBuffer_test.cpp @@ -98,4 +98,20 @@ TEST(BigBufferTest, PadAndAlignProperly) { ASSERT_EQ(8u, buffer.size()); } +TEST(BigBufferTest, BackUpZeroed) { + BigBuffer buffer(16); + + auto block = buffer.NextBlock<char>(2); + ASSERT_TRUE(block != nullptr); + ASSERT_EQ(2u, buffer.size()); + block[0] = 0x01; + block[1] = 0x02; + buffer.BackUp(1); + ASSERT_EQ(1u, buffer.size()); + auto new_block = buffer.NextBlock<char>(1); + ASSERT_TRUE(new_block != nullptr); + ASSERT_EQ(2u, buffer.size()); + ASSERT_EQ(0, *new_block); +} + } // namespace android diff --git a/libs/androidfw/tests/CombinedIterator_test.cpp b/libs/androidfw/tests/CombinedIterator_test.cpp new file mode 100644 index 000000000000..c1228f34625f --- /dev/null +++ b/libs/androidfw/tests/CombinedIterator_test.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "androidfw/CombinedIterator.h" + +#include <algorithm> +#include <string> +#include <strstream> +#include <utility> +#include <vector> + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace android { + +template <class Coll> +std::string toString(const Coll& coll) { + std::stringstream res; + res << "(" << std::size(coll) << ")"; + if (std::size(coll)) { + res << "{" << coll[0]; + for (int i = 1; i != std::size(coll); ++i) { + res << "," << coll[i]; + } + res << "}"; + } + return res.str(); +} + +template <class Coll> +void AssertCollectionEq(const Coll& first, const Coll& second) { + ASSERT_EQ(std::size(first), std::size(second)) + << "first: " << toString(first) << ", second: " << toString(second); + for (int i = 0; i != std::size(first); ++i) { + ASSERT_EQ(first[i], second[i]) + << "index: " << i << " first: " << toString(first) << ", second: " << toString(second); + } +} + +TEST(CombinedIteratorTest, Sorting) { + std::vector<int> v1 = {2, 1, 3, 4, 0}; + std::vector<int> v2 = {20, 10, 30, 40, 0}; + + std::sort(CombinedIterator(v1.begin(), v2.begin()), CombinedIterator(v1.end(), v2.end())); + + ASSERT_EQ(v1.size(), v2.size()); + ASSERT_TRUE(std::is_sorted(v1.begin(), v1.end())); + ASSERT_TRUE(std::is_sorted(v2.begin(), v2.end())); + AssertCollectionEq(v1, {0, 1, 2, 3, 4}); + AssertCollectionEq(v2, {0, 10, 20, 30, 40}); +} + +TEST(CombinedIteratorTest, Removing) { + std::vector<int> v1 = {1, 2, 3, 4, 5, 5, 5, 6}; + std::vector<int> v2 = {10, 20, 30, 40, 50, 50, 50, 60}; + + auto newEnd = + std::remove_if(CombinedIterator(v1.begin(), v2.begin()), CombinedIterator(v1.end(), v2.end()), + [](auto&& pair) { return pair.first >= 3 && pair.first <= 5; }); + + ASSERT_EQ(newEnd.it1, v1.begin() + 3); + ASSERT_EQ(newEnd.it2, v2.begin() + 3); + + v1.erase(newEnd.it1, v1.end()); + AssertCollectionEq(v1, {1, 2, 6}); + v2.erase(newEnd.it2, v2.end()); + AssertCollectionEq(v2, {10, 20, 60}); +} + +TEST(CombinedIteratorTest, InplaceMerge) { + std::vector<int> v1 = {1, 3, 4, 7, 2, 5, 6}; + std::vector<int> v2 = {10, 30, 40, 70, 20, 50, 60}; + + std::inplace_merge(CombinedIterator(v1.begin(), v2.begin()), + CombinedIterator(v1.begin() + 4, v2.begin() + 4), + CombinedIterator(v1.end(), v2.end())); + ASSERT_TRUE(std::is_sorted(v1.begin(), v1.end())); + ASSERT_TRUE(std::is_sorted(v2.begin(), v2.end())); + + AssertCollectionEq(v1, {1, 2, 3, 4, 5, 6, 7}); + AssertCollectionEq(v2, {10, 20, 30, 40, 50, 60, 70}); +} + +} // namespace android diff --git a/libs/androidfw/tests/Theme_bench.cpp b/libs/androidfw/tests/Theme_bench.cpp index dfbb5a76dec6..bf89617635cc 100644 --- a/libs/androidfw/tests/Theme_bench.cpp +++ b/libs/androidfw/tests/Theme_bench.cpp @@ -27,6 +27,10 @@ constexpr const static char* kFrameworkPath = "/system/framework/framework-res.a constexpr const static uint32_t kStyleId = 0x01030237u; // android:style/Theme.Material.Light constexpr const static uint32_t kAttrId = 0x01010030u; // android:attr/colorForeground +constexpr const static uint32_t kStyle2Id = 0x01030224u; // android:style/Theme.Material +constexpr const static uint32_t kStyle3Id = 0x0103024du; // android:style/Widget.Material +constexpr const static uint32_t kStyle4Id = 0x0103028eu; // android:style/Widget.Material.Light + static void BM_ThemeApplyStyleFramework(benchmark::State& state) { auto apk = ApkAssets::Load(kFrameworkPath); if (apk == nullptr) { @@ -61,6 +65,32 @@ static void BM_ThemeApplyStyleFrameworkOld(benchmark::State& state) { } BENCHMARK(BM_ThemeApplyStyleFrameworkOld); +static void BM_ThemeRebaseFramework(benchmark::State& state) { + auto apk = ApkAssets::Load(kFrameworkPath); + if (apk == nullptr) { + state.SkipWithError("Failed to load assets"); + return; + } + + AssetManager2 assets; + assets.SetApkAssets({apk}); + + // Create two arrays of styles to switch between back and forth. + const uint32_t styles1[] = {kStyle2Id, kStyleId, kStyle3Id}; + const uint8_t force1[std::size(styles1)] = {false, true, false}; + const uint32_t styles2[] = {kStyleId, kStyle2Id, kStyle4Id, kStyle3Id}; + const uint8_t force2[std::size(styles2)] = {false, true, true, false}; + const auto theme = assets.NewTheme(); + // Initialize the theme to make the first iteration the same as the rest. + theme->Rebase(&assets, styles1, force1, std::size(force1)); + + while (state.KeepRunning()) { + theme->Rebase(&assets, styles2, force2, std::size(force2)); + theme->Rebase(&assets, styles1, force1, std::size(force1)); + } +} +BENCHMARK(BM_ThemeRebaseFramework); + static void BM_ThemeGetAttribute(benchmark::State& state) { auto apk = ApkAssets::Load(kFrameworkPath); diff --git a/libs/androidfw/tests/Theme_test.cpp b/libs/androidfw/tests/Theme_test.cpp index 181d1411fb91..afcb0c1ad964 100644 --- a/libs/androidfw/tests/Theme_test.cpp +++ b/libs/androidfw/tests/Theme_test.cpp @@ -260,7 +260,7 @@ TEST_F(ThemeTest, ThemeRebase) { ResTable_config night{}; night.uiMode = ResTable_config::UI_MODE_NIGHT_YES; night.version = 8u; - am_night.SetConfigurations({night}); + am_night.SetConfigurations({{night}}); auto theme = am.NewTheme(); { diff --git a/libs/appfunctions/Android.bp b/libs/appfunctions/Android.bp new file mode 100644 index 000000000000..c6cee07d1946 --- /dev/null +++ b/libs/appfunctions/Android.bp @@ -0,0 +1,39 @@ +// Copyright (C) 2024 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_sdk_library { + name: "com.google.android.appfunctions.sidecar", + owner: "google", + srcs: ["java/**/*.java"], + api_packages: ["com.google.android.appfunctions.sidecar"], + dex_preopt: { + enabled: false, + }, + system_ext_specific: true, + no_dist: true, + unsafe_ignore_missing_latest_api: true, +} + +prebuilt_etc { + name: "appfunctions.sidecar.xml", + system_ext_specific: true, + sub_dir: "permissions", + src: "appfunctions.sidecar.xml", + filename_from_src: true, +} diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt new file mode 100644 index 000000000000..504e3290b0ae --- /dev/null +++ b/libs/appfunctions/api/current.txt @@ -0,0 +1,49 @@ +// Signature format: 2.0 +package com.google.android.appfunctions.sidecar { + + public final class AppFunctionManager { + ctor public AppFunctionManager(android.content.Context); + method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + } + + public abstract class AppFunctionService extends android.app.Service { + ctor public AppFunctionService(); + method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); + method @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE"; + field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; + } + + public final class ExecuteAppFunctionRequest { + method @NonNull public android.os.Bundle getExtras(); + method @NonNull public String getFunctionIdentifier(); + method @NonNull public android.app.appsearch.GenericDocument getParameters(); + method @NonNull public String getTargetPackageName(); + } + + public static final class ExecuteAppFunctionRequest.Builder { + ctor public ExecuteAppFunctionRequest.Builder(@NonNull String, @NonNull String); + method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest build(); + method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder setExtras(@NonNull android.os.Bundle); + method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder setParameters(@NonNull android.app.appsearch.GenericDocument); + } + + public final class ExecuteAppFunctionResponse { + method @Nullable public String getErrorMessage(); + method @NonNull public android.os.Bundle getExtras(); + method public int getResultCode(); + method @NonNull public android.app.appsearch.GenericDocument getResultDocument(); + method public boolean isSuccess(); + method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newFailure(int, @Nullable String, @Nullable android.os.Bundle); + method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newSuccess(@NonNull android.app.appsearch.GenericDocument, @Nullable android.os.Bundle); + field public static final String PROPERTY_RETURN_VALUE = "returnValue"; + field public static final int RESULT_APP_UNKNOWN_ERROR = 2; // 0x2 + field public static final int RESULT_DENIED = 1; // 0x1 + field public static final int RESULT_INTERNAL_ERROR = 3; // 0x3 + field public static final int RESULT_INVALID_ARGUMENT = 4; // 0x4 + field public static final int RESULT_OK = 0; // 0x0 + field public static final int RESULT_TIMED_OUT = 5; // 0x5 + } + +} + diff --git a/libs/appfunctions/api/removed.txt b/libs/appfunctions/api/removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/libs/appfunctions/api/removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/libs/appfunctions/api/system-current.txt b/libs/appfunctions/api/system-current.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/libs/appfunctions/api/system-current.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/libs/appfunctions/api/system-removed.txt b/libs/appfunctions/api/system-removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/libs/appfunctions/api/system-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/libs/appfunctions/api/test-current.txt b/libs/appfunctions/api/test-current.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/libs/appfunctions/api/test-current.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/libs/appfunctions/api/test-removed.txt b/libs/appfunctions/api/test-removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/libs/appfunctions/api/test-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/libs/appfunctions/appfunctions.sidecar.xml b/libs/appfunctions/appfunctions.sidecar.xml new file mode 100644 index 000000000000..bef8b6ec7ce6 --- /dev/null +++ b/libs/appfunctions/appfunctions.sidecar.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 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. + --> +<permissions> + <library + name="com.google.android.appfunctions.sidecar" + file="/system_ext/framework/com.google.android.appfunctions.sidecar.jar"/> +</permissions>
\ No newline at end of file diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java new file mode 100644 index 000000000000..b1dd4676a35e --- /dev/null +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2024 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.google.android.appfunctions.sidecar; + +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.content.Context; + +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + + +/** + * Provides app functions related functionalities. + * + * <p>App function is a specific piece of functionality that an app offers to the system. These + * functionalities can be integrated into various system features. + * + * <p>This class wraps {@link android.app.appfunctions.AppFunctionManager} functionalities and + * exposes it here as a sidecar library (avoiding direct dependency on the platform API). + */ +// TODO(b/357551503): Implement get and set enabled app function APIs. +// TODO(b/367329899): Add sidecar library to Android B builds. +public final class AppFunctionManager { + private final android.app.appfunctions.AppFunctionManager mManager; + private final Context mContext; + + /** + * Creates an instance. + * + * @param context A {@link Context}. + * @throws java.lang.IllegalStateException if the underlying {@link + * android.app.appfunctions.AppFunctionManager} is not found. + */ + public AppFunctionManager(Context context) { + mContext = Objects.requireNonNull(context); + mManager = context.getSystemService(android.app.appfunctions.AppFunctionManager.class); + if (mManager == null) { + throw new IllegalStateException( + "Underlying AppFunctionManager system service not found."); + } + } + + /** + * Executes the app function. + * + * <p>Proxies request and response to the underlying {@link + * android.app.appfunctions.AppFunctionManager#executeAppFunction}, converting the request and + * response in the appropriate type required by the function. + */ + public void executeAppFunction( + @NonNull ExecuteAppFunctionRequest sidecarRequest, + @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<ExecuteAppFunctionResponse> callback) { + Objects.requireNonNull(sidecarRequest); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + + android.app.appfunctions.ExecuteAppFunctionRequest platformRequest = + SidecarConverter.getPlatformExecuteAppFunctionRequest(sidecarRequest); + mManager.executeAppFunction( + platformRequest, executor, (platformResponse) -> { + callback.accept(SidecarConverter.getSidecarExecuteAppFunctionResponse( + platformResponse)); + }); + } +} diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java new file mode 100644 index 000000000000..65959dfdf561 --- /dev/null +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2024 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.google.android.appfunctions.sidecar; + +import static android.Manifest.permission.BIND_APP_FUNCTION_SERVICE; + +import android.annotation.MainThread; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; + +import java.util.function.Consumer; + +/** + * Abstract base class to provide app functions to the system. + * + * <p>Include the following in the manifest: + * + * <pre> + * {@literal + * <service android:name=".YourService" + * android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"> + * <intent-filter> + * <action android:name="android.app.appfunctions.AppFunctionService" /> + * </intent-filter> + * </service> + * } + * </pre> + * + * <p>This class wraps {@link android.app.appfunctions.AppFunctionService} functionalities and + * exposes it here as a sidecar library (avoiding direct dependency on the platform API). + * + * @see AppFunctionManager + */ +public abstract class AppFunctionService extends Service { + /** + * The permission to only allow system access to the functions through {@link + * AppFunctionManagerService}. + */ + @NonNull + public static final String BIND_APP_FUNCTION_SERVICE = + "android.permission.BIND_APP_FUNCTION_SERVICE"; + + /** + * The {@link Intent} that must be declared as handled by the service. To be supported, the + * service must also require the {@link BIND_APP_FUNCTION_SERVICE} permission so that other + * applications can not abuse it. + */ + @NonNull + public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; + + private final Binder mBinder = + android.app.appfunctions.AppFunctionService.createBinder( + /* context= */ this, + /* onExecuteFunction= */ (platformRequest, callback) -> { + AppFunctionService.this.onExecuteFunction( + SidecarConverter.getSidecarExecuteAppFunctionRequest( + platformRequest), + (sidecarResponse) -> { + callback.accept( + SidecarConverter.getPlatformExecuteAppFunctionResponse( + sidecarResponse)); + }); + } + ); + + @NonNull + @Override + public final IBinder onBind(@Nullable Intent intent) { + return mBinder; + } + + /** + * Called by the system to execute a specific app function. + * + * <p>This method is triggered when the system requests your AppFunctionService to handle a + * particular function you have registered and made available. + * + * <p>To ensure proper routing of function requests, assign a unique identifier to each + * function. This identifier doesn't need to be globally unique, but it must be unique within + * your app. For example, a function to order food could be identified as "orderFood". In most + * cases this identifier should come from the ID automatically generated by the AppFunctions + * SDK. You can determine the specific function to invoke by calling {@link + * ExecuteAppFunctionRequest#getFunctionIdentifier()}. + * + * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker + * thread and dispatch the result with the given callback. You should always report back the + * result using the callback, no matter if the execution was successful or not. + * + * @param request The function execution request. + * @param callback A callback to report back the result. + */ + @MainThread + public abstract void onExecuteFunction( + @NonNull ExecuteAppFunctionRequest request, + @NonNull Consumer<ExecuteAppFunctionResponse> callback); +} diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java new file mode 100644 index 000000000000..fa6d2ff12313 --- /dev/null +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2024 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.google.android.appfunctions.sidecar; + +import android.annotation.NonNull; +import android.app.appsearch.GenericDocument; +import android.os.Bundle; + +import java.util.Objects; + +/** + * A request to execute an app function. + * + * <p>This class copies {@link android.app.appfunctions.ExecuteAppFunctionRequest} without parcel + * functionality and exposes it here as a sidecar library (avoiding direct dependency on the + * platform API). + */ +public final class ExecuteAppFunctionRequest { + /** Returns the package name of the app that hosts the function. */ + @NonNull private final String mTargetPackageName; + + /** + * Returns the unique string identifier of the app function to be executed. TODO(b/357551503): + * Document how callers can get the available function identifiers. + */ + @NonNull private final String mFunctionIdentifier; + + /** Returns additional metadata relevant to this function execution request. */ + @NonNull private final Bundle mExtras; + + /** + * Returns the parameters required to invoke this function. Within this [GenericDocument], the + * property names are the names of the function parameters and the property values are the + * values of those parameters. + * + * <p>The document may have missing parameters. Developers are advised to implement defensive + * handling measures. + * + * <p>TODO(b/357551503): Document how function parameters can be obtained for function execution + */ + @NonNull private final GenericDocument mParameters; + + private ExecuteAppFunctionRequest( + @NonNull String targetPackageName, + @NonNull String functionIdentifier, + @NonNull Bundle extras, + @NonNull GenericDocument parameters) { + mTargetPackageName = Objects.requireNonNull(targetPackageName); + mFunctionIdentifier = Objects.requireNonNull(functionIdentifier); + mExtras = Objects.requireNonNull(extras); + mParameters = Objects.requireNonNull(parameters); + } + + /** Returns the package name of the app that hosts the function. */ + @NonNull + public String getTargetPackageName() { + return mTargetPackageName; + } + + /** Returns the unique string identifier of the app function to be executed. */ + @NonNull + public String getFunctionIdentifier() { + return mFunctionIdentifier; + } + + /** + * Returns the function parameters. The key is the parameter name, and the value is the + * parameter value. + * + * <p>The bundle may have missing parameters. Developers are advised to implement defensive + * handling measures. + */ + @NonNull + public GenericDocument getParameters() { + return mParameters; + } + + /** Returns the additional data relevant to this function execution. */ + @NonNull + public Bundle getExtras() { + return mExtras; + } + + /** Builder for {@link ExecuteAppFunctionRequest}. */ + public static final class Builder { + @NonNull private final String mTargetPackageName; + @NonNull private final String mFunctionIdentifier; + @NonNull private Bundle mExtras = Bundle.EMPTY; + + @NonNull + private GenericDocument mParameters = new GenericDocument.Builder<>("", "", "").build(); + + public Builder(@NonNull String targetPackageName, @NonNull String functionIdentifier) { + mTargetPackageName = Objects.requireNonNull(targetPackageName); + mFunctionIdentifier = Objects.requireNonNull(functionIdentifier); + } + + /** Sets the additional data relevant to this function execution. */ + @NonNull + public Builder setExtras(@NonNull Bundle extras) { + mExtras = Objects.requireNonNull(extras); + return this; + } + + /** Sets the function parameters. */ + @NonNull + public Builder setParameters(@NonNull GenericDocument parameters) { + Objects.requireNonNull(parameters); + mParameters = parameters; + return this; + } + + /** Builds the {@link ExecuteAppFunctionRequest}. */ + @NonNull + public ExecuteAppFunctionRequest build() { + return new ExecuteAppFunctionRequest( + mTargetPackageName, + mFunctionIdentifier, + mExtras, + mParameters); + } + } +} diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java new file mode 100644 index 000000000000..60c25fae58d1 --- /dev/null +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2024 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.google.android.appfunctions.sidecar; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.appsearch.GenericDocument; +import android.os.Bundle; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * The response to an app function execution. + * + * <p>This class copies {@link android.app.appfunctions.ExecuteAppFunctionResponse} without parcel + * functionality and exposes it here as a sidecar library (avoiding direct dependency on the + * platform API). + */ +public final class ExecuteAppFunctionResponse { + /** + * The name of the property that stores the function return value within the {@code + * resultDocument}. + * + * <p>See {@link GenericDocument#getProperty(String)} for more information. + * + * <p>If the function returns {@code void} or throws an error, the {@code resultDocument} will + * be empty {@link GenericDocument}. + * + * <p>If the {@code resultDocument} is empty, {@link GenericDocument#getProperty(String)} will + * return {@code null}. + * + * <p>See {@link #getResultDocument} for more information on extracting the return value. + */ + public static final String PROPERTY_RETURN_VALUE = "returnValue"; + + /** The call was successful. */ + public static final int RESULT_OK = 0; + + /** The caller does not have the permission to execute an app function. */ + public static final int RESULT_DENIED = 1; + + /** An unknown error occurred while processing the call in the AppFunctionService. */ + public static final int RESULT_APP_UNKNOWN_ERROR = 2; + + /** + * An internal error occurred within AppFunctionManagerService. + * + * <p>This error may be considered similar to {@link IllegalStateException} + */ + public static final int RESULT_INTERNAL_ERROR = 3; + + /** + * The caller supplied invalid arguments to the call. + * + * <p>This error may be considered similar to {@link IllegalArgumentException}. + */ + public static final int RESULT_INVALID_ARGUMENT = 4; + + /** The operation was timed out. */ + public static final int RESULT_TIMED_OUT = 5; + + /** The result code of the app function execution. */ + @ResultCode private final int mResultCode; + + /** + * The error message associated with the result, if any. This is {@code null} if the result code + * is {@link #RESULT_OK}. + */ + @Nullable private final String mErrorMessage; + + /** + * Returns the return value of the executed function. + * + * <p>The return value is stored in a {@link GenericDocument} with the key {@link + * #PROPERTY_RETURN_VALUE}. + * + * <p>See {@link #getResultDocument} for more information on extracting the return value. + */ + @NonNull private final GenericDocument mResultDocument; + + /** Returns the additional metadata data relevant to this function execution response. */ + @NonNull private final Bundle mExtras; + + private ExecuteAppFunctionResponse( + @NonNull GenericDocument resultDocument, + @NonNull Bundle extras, + @ResultCode int resultCode, + @Nullable String errorMessage) { + mResultDocument = Objects.requireNonNull(resultDocument); + mExtras = Objects.requireNonNull(extras); + mResultCode = resultCode; + mErrorMessage = errorMessage; + } + + /** + * Returns result codes from throwable. + * + * @hide + */ + static @ResultCode int getResultCode(@NonNull Throwable t) { + if (t instanceof IllegalArgumentException) { + return ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT; + } + return ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR; + } + + /** + * Returns a successful response. + * + * @param resultDocument The return value of the executed function. + * @param extras The additional metadata data relevant to this function execution response. + */ + @NonNull + public static ExecuteAppFunctionResponse newSuccess( + @NonNull GenericDocument resultDocument, @Nullable Bundle extras) { + Objects.requireNonNull(resultDocument); + Bundle actualExtras = getActualExtras(extras); + + return new ExecuteAppFunctionResponse( + resultDocument, actualExtras, RESULT_OK, /* errorMessage= */ null); + } + + /** + * Returns a failure response. + * + * @param resultCode The result code of the app function execution. + * @param extras The additional metadata data relevant to this function execution response. + * @param errorMessage The error message associated with the result, if any. + */ + @NonNull + public static ExecuteAppFunctionResponse newFailure( + @ResultCode int resultCode, @Nullable String errorMessage, @Nullable Bundle extras) { + if (resultCode == RESULT_OK) { + throw new IllegalArgumentException("resultCode must not be RESULT_OK"); + } + Bundle actualExtras = getActualExtras(extras); + GenericDocument emptyDocument = new GenericDocument.Builder<>("", "", "").build(); + return new ExecuteAppFunctionResponse( + emptyDocument, actualExtras, resultCode, errorMessage); + } + + private static Bundle getActualExtras(@Nullable Bundle extras) { + if (extras == null) { + return Bundle.EMPTY; + } + return extras; + } + + /** + * Returns a generic document containing the return value of the executed function. + * + * <p>The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value. + * + * <p>An empty document is returned if {@link #isSuccess} is {@code false} or if the executed + * function does not produce a return value. + * + * <p>Sample code for extracting the return value: + * + * <pre> + * GenericDocument resultDocument = response.getResultDocument(); + * Object returnValue = resultDocument.getProperty(PROPERTY_RETURN_VALUE); + * if (returnValue != null) { + * // Cast returnValue to expected type, or use {@link GenericDocument#getPropertyString}, + * // {@link GenericDocument#getPropertyLong} etc. + * // Do something with the returnValue + * } + * </pre> + */ + @NonNull + public GenericDocument getResultDocument() { + return mResultDocument; + } + + /** Returns the extras of the app function execution. */ + @NonNull + public Bundle getExtras() { + return mExtras; + } + + /** + * Returns {@code true} if {@link #getResultCode} equals {@link + * ExecuteAppFunctionResponse#RESULT_OK}. + */ + public boolean isSuccess() { + return getResultCode() == RESULT_OK; + } + + /** + * Returns one of the {@code RESULT} constants defined in {@link ExecuteAppFunctionResponse}. + */ + @ResultCode + public int getResultCode() { + return mResultCode; + } + + /** + * Returns the error message associated with this result. + * + * <p>If {@link #isSuccess} is {@code true}, the error message is always {@code null}. + */ + @Nullable + public String getErrorMessage() { + return mErrorMessage; + } + + /** + * Result codes. + * + * @hide + */ + @IntDef( + prefix = {"RESULT_"}, + value = { + RESULT_OK, + RESULT_DENIED, + RESULT_APP_UNKNOWN_ERROR, + RESULT_INTERNAL_ERROR, + RESULT_INVALID_ARGUMENT, + RESULT_TIMED_OUT, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ResultCode {} +} diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java new file mode 100644 index 000000000000..b1b05f79f33f --- /dev/null +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 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.google.android.appfunctions.sidecar; + +import android.annotation.NonNull; + +/** + * Utility class containing methods to convert Sidecar objects of AppFunctions API into the + * underlying platform classes. + * + * @hide + */ +public final class SidecarConverter { + private SidecarConverter() {} + + /** + * Converts sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest} + * into platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest} + * + * @hide + */ + @NonNull + public static android.app.appfunctions.ExecuteAppFunctionRequest + getPlatformExecuteAppFunctionRequest(@NonNull ExecuteAppFunctionRequest request) { + return new + android.app.appfunctions.ExecuteAppFunctionRequest.Builder( + request.getTargetPackageName(), + request.getFunctionIdentifier()) + .setExtras(request.getExtras()) + .setParameters(request.getParameters()) + .build(); + } + + /** + * Converts sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse} + * into platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse} + * + * @hide + */ + @NonNull + public static android.app.appfunctions.ExecuteAppFunctionResponse + getPlatformExecuteAppFunctionResponse(@NonNull ExecuteAppFunctionResponse response) { + if (response.isSuccess()) { + return android.app.appfunctions.ExecuteAppFunctionResponse.newSuccess( + response.getResultDocument(), response.getExtras()); + } else { + return android.app.appfunctions.ExecuteAppFunctionResponse.newFailure( + response.getResultCode(), + response.getErrorMessage(), + response.getExtras()); + } + } + + /** + * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest} + * into sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest} + * + * @hide + */ + @NonNull + public static ExecuteAppFunctionRequest getSidecarExecuteAppFunctionRequest( + @NonNull android.app.appfunctions.ExecuteAppFunctionRequest request) { + return new ExecuteAppFunctionRequest.Builder( + request.getTargetPackageName(), + request.getFunctionIdentifier()) + .setExtras(request.getExtras()) + .setParameters(request.getParameters()) + .build(); + } + + /** + * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse} + * into sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse} + * + * @hide + */ + @NonNull + public static ExecuteAppFunctionResponse getSidecarExecuteAppFunctionResponse( + @NonNull android.app.appfunctions.ExecuteAppFunctionResponse response) { + if (response.isSuccess()) { + return ExecuteAppFunctionResponse.newSuccess( + response.getResultDocument(), response.getExtras()); + } else { + return ExecuteAppFunctionResponse.newFailure( + response.getResultCode(), + response.getErrorMessage(), + response.getExtras()); + } + } +} diff --git a/libs/hostgraphics/Android.bp b/libs/hostgraphics/Android.bp index 09232b64616d..a58493aa47ca 100644 --- a/libs/hostgraphics/Android.bp +++ b/libs/hostgraphics/Android.bp @@ -7,6 +7,17 @@ package { default_applicable_licenses: ["frameworks_base_license"], } +cc_library_headers { + name: "libhostgraphics_headers", + host_supported: true, + export_include_dirs: ["include"], + target: { + windows: { + enabled: true, + }, + }, +} + cc_library_host_static { name: "libhostgraphics", @@ -30,12 +41,13 @@ cc_library_host_static { ], header_libs: [ + "libhostgraphics_headers", "libnativebase_headers", "libnativedisplay_headers", "libnativewindow_headers", ], - export_include_dirs: ["."], + export_include_dirs: ["include"], target: { windows: { diff --git a/libs/hostgraphics/gui/BufferItem.h b/libs/hostgraphics/include/gui/BufferItem.h index e95a9231dfaf..e95a9231dfaf 100644 --- a/libs/hostgraphics/gui/BufferItem.h +++ b/libs/hostgraphics/include/gui/BufferItem.h diff --git a/libs/hostgraphics/gui/BufferItemConsumer.h b/libs/hostgraphics/include/gui/BufferItemConsumer.h index c25941151800..c25941151800 100644 --- a/libs/hostgraphics/gui/BufferItemConsumer.h +++ b/libs/hostgraphics/include/gui/BufferItemConsumer.h diff --git a/libs/hostgraphics/gui/BufferQueue.h b/libs/hostgraphics/include/gui/BufferQueue.h index 67a8c00fd267..67a8c00fd267 100644 --- a/libs/hostgraphics/gui/BufferQueue.h +++ b/libs/hostgraphics/include/gui/BufferQueue.h diff --git a/libs/hostgraphics/gui/ConsumerBase.h b/libs/hostgraphics/include/gui/ConsumerBase.h index 7f7309e8a3a8..7f7309e8a3a8 100644 --- a/libs/hostgraphics/gui/ConsumerBase.h +++ b/libs/hostgraphics/include/gui/ConsumerBase.h diff --git a/libs/hostgraphics/gui/IGraphicBufferConsumer.h b/libs/hostgraphics/include/gui/IGraphicBufferConsumer.h index 14ac4fe71cc8..14ac4fe71cc8 100644 --- a/libs/hostgraphics/gui/IGraphicBufferConsumer.h +++ b/libs/hostgraphics/include/gui/IGraphicBufferConsumer.h diff --git a/libs/hostgraphics/gui/IGraphicBufferProducer.h b/libs/hostgraphics/include/gui/IGraphicBufferProducer.h index 8fd8590d10d7..8fd8590d10d7 100644 --- a/libs/hostgraphics/gui/IGraphicBufferProducer.h +++ b/libs/hostgraphics/include/gui/IGraphicBufferProducer.h diff --git a/libs/hostgraphics/gui/Surface.h b/libs/hostgraphics/include/gui/Surface.h index 2774f89cb54c..2774f89cb54c 100644 --- a/libs/hostgraphics/gui/Surface.h +++ b/libs/hostgraphics/include/gui/Surface.h diff --git a/libs/hostgraphics/ui/Fence.h b/libs/hostgraphics/include/ui/Fence.h index 187c3116f61c..187c3116f61c 100644 --- a/libs/hostgraphics/ui/Fence.h +++ b/libs/hostgraphics/include/ui/Fence.h diff --git a/libs/hostgraphics/ui/GraphicBuffer.h b/libs/hostgraphics/include/ui/GraphicBuffer.h index cda45e4660ca..cda45e4660ca 100644 --- a/libs/hostgraphics/ui/GraphicBuffer.h +++ b/libs/hostgraphics/include/ui/GraphicBuffer.h diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 341599e79662..23cd3ce965ff 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -114,16 +114,12 @@ cc_defaults { "libbase", "libharfbuzz_ng", "libminikin", - "server_configurable_flags", - "libaconfig_storage_read_api_cc" ], static_libs: [ "libui-types", ], - whole_static_libs: ["hwui_flags_cc_lib"], - target: { android: { shared_libs: [ @@ -145,6 +141,9 @@ cc_defaults { "libsync", "libui", "aconfig_text_flags_c_lib", + "server_configurable_flags", + "libaconfig_storage_read_api_cc", + "libgraphicsenv", ], static_libs: [ "libEGL_blobCache", @@ -155,6 +154,7 @@ cc_defaults { "libstatssocket_lazy", "libtonemap", ], + whole_static_libs: ["hwui_flags_cc_lib"], }, host: { static_libs: [ @@ -419,7 +419,6 @@ cc_defaults { ], static_libs: [ - "libnativehelper_lazy", "libziparchive_for_incfs", ], @@ -446,6 +445,7 @@ cc_defaults { ], static_libs: [ "libgif", + "libnativehelper_lazy", "libstatslog_hwui", "libstatspull_lazy", "libstatssocket_lazy", @@ -464,6 +464,7 @@ cc_defaults { ], static_libs: [ "libandroidfw", + "libnativehelper_jvm", ], }, }, @@ -736,6 +737,7 @@ cc_defaults { cc_test { name: "hwui_unit_tests", + test_config: "tests/unit/AndroidTest.xml", defaults: [ "hwui_test_defaults", "android_graphics_apex", @@ -803,6 +805,7 @@ cc_test { cc_benchmark { name: "hwuimacro", + test_config: "tests/macrobench/AndroidTest.xml", defaults: ["hwui_test_defaults"], static_libs: ["libhwui"], @@ -822,6 +825,7 @@ cc_benchmark { cc_benchmark { name: "hwuimicro", + test_config: "tests/microbench/AndroidTest.xml", defaults: ["hwui_test_defaults"], static_libs: ["libhwui_static"], diff --git a/libs/hwui/AutoBackendTextureRelease.cpp b/libs/hwui/AutoBackendTextureRelease.cpp index 86fede5eb98c..430519606d9b 100644 --- a/libs/hwui/AutoBackendTextureRelease.cpp +++ b/libs/hwui/AutoBackendTextureRelease.cpp @@ -17,11 +17,12 @@ #include "AutoBackendTextureRelease.h" #include <SkImage.h> -#include <include/gpu/ganesh/SkImageGanesh.h> -#include <include/gpu/GrDirectContext.h> -#include <include/gpu/GrBackendSurface.h> #include <include/gpu/MutableTextureState.h> +#include <include/gpu/ganesh/GrBackendSurface.h> +#include <include/gpu/ganesh/GrDirectContext.h> +#include <include/gpu/ganesh/SkImageGanesh.h> #include <include/gpu/vk/VulkanMutableTextureState.h> + #include "renderthread/RenderThread.h" #include "utils/Color.h" #include "utils/PaintUtils.h" diff --git a/libs/hwui/AutoBackendTextureRelease.h b/libs/hwui/AutoBackendTextureRelease.h index f0eb2a8b6eab..d58cd1787ee8 100644 --- a/libs/hwui/AutoBackendTextureRelease.h +++ b/libs/hwui/AutoBackendTextureRelease.h @@ -16,10 +16,10 @@ #pragma once -#include <GrAHardwareBufferUtils.h> -#include <GrBackendSurface.h> #include <SkImage.h> #include <android/hardware_buffer.h> +#include <include/android/GrAHardwareBufferUtils.h> +#include <include/gpu/ganesh/GrBackendSurface.h> #include <system/graphics.h> namespace android { diff --git a/libs/hwui/ColorFilter.h b/libs/hwui/ColorFilter.h index 31c9db7ca4fb..3a3bfb47dfdd 100644 --- a/libs/hwui/ColorFilter.h +++ b/libs/hwui/ColorFilter.h @@ -106,7 +106,7 @@ public: private: sk_sp<SkColorFilter> createInstance() override { - return SkColorFilters::Matrix(mMatrix.data()); + return SkColorFilters::Matrix(mMatrix.data(), SkColorFilters::Clamp::kNo); } private: diff --git a/libs/hwui/FeatureFlags.h b/libs/hwui/FeatureFlags.h index ac75c077b58f..fddcf29b9197 100644 --- a/libs/hwui/FeatureFlags.h +++ b/libs/hwui/FeatureFlags.h @@ -25,25 +25,18 @@ namespace android { namespace text_feature { -inline bool fix_double_underline() { -#ifdef __ANDROID__ - return com_android_text_flags_fix_double_underline(); -#else - return true; -#endif // __ANDROID__ -} - -inline bool deprecate_ui_fonts() { +inline bool letter_spacing_justification() { #ifdef __ANDROID__ - return com_android_text_flags_deprecate_ui_fonts(); + return com_android_text_flags_letter_spacing_justification(); #else return true; #endif // __ANDROID__ } -inline bool letter_spacing_justification() { +inline bool typeface_redesign() { #ifdef __ANDROID__ - return com_android_text_flags_letter_spacing_justification(); + static bool flag = com_android_text_flags_typeface_redesign(); + return flag; #else return true; #endif // __ANDROID__ diff --git a/libs/hwui/HardwareBitmapUploader.cpp b/libs/hwui/HardwareBitmapUploader.cpp index 27ea15075682..236c3736816e 100644 --- a/libs/hwui/HardwareBitmapUploader.cpp +++ b/libs/hwui/HardwareBitmapUploader.cpp @@ -21,8 +21,6 @@ #include <GLES2/gl2.h> #include <GLES2/gl2ext.h> #include <GLES3/gl3.h> -#include <GrDirectContext.h> -#include <GrTypes.h> #include <SkBitmap.h> #include <SkCanvas.h> #include <SkImage.h> @@ -30,6 +28,8 @@ #include <SkImageInfo.h> #include <SkRefCnt.h> #include <gui/TraceUtils.h> +#include <include/gpu/ganesh/GrDirectContext.h> +#include <include/gpu/ganesh/GrTypes.h> #include <utils/GLUtils.h> #include <utils/NdkUtils.h> #include <utils/Trace.h> @@ -318,6 +318,11 @@ bool HardwareBitmapUploader::has1010102Support() { return has101012Support; } +bool HardwareBitmapUploader::has10101010Support() { + static bool has1010110Support = checkSupport(AHARDWAREBUFFER_FORMAT_R10G10B10A10_UNORM); + return has1010110Support; +} + bool HardwareBitmapUploader::hasAlpha8Support() { static bool hasAlpha8Support = checkSupport(AHARDWAREBUFFER_FORMAT_R8_UNORM); return hasAlpha8Support; @@ -376,6 +381,19 @@ static FormatInfo determineFormat(const SkBitmap& skBitmap, bool usingGL) { } formatInfo.format = GL_RGBA; break; + case kRGBA_10x6_SkColorType: + formatInfo.isSupported = HardwareBitmapUploader::has10101010Support(); + if (formatInfo.isSupported) { + formatInfo.type = 0; // Not supported in GL + formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R10G10B10A10_UNORM; + formatInfo.vkFormat = VK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16; + } else { + formatInfo.type = GL_UNSIGNED_BYTE; + formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; + formatInfo.vkFormat = VK_FORMAT_R8G8B8A8_UNORM; + } + formatInfo.format = 0; // Not supported in GL + break; case kAlpha_8_SkColorType: formatInfo.isSupported = HardwareBitmapUploader::hasAlpha8Support(); if (formatInfo.isSupported) { diff --git a/libs/hwui/HardwareBitmapUploader.h b/libs/hwui/HardwareBitmapUploader.h index 00ee99648889..76cb80b722d0 100644 --- a/libs/hwui/HardwareBitmapUploader.h +++ b/libs/hwui/HardwareBitmapUploader.h @@ -33,12 +33,14 @@ public: #ifdef __ANDROID__ static bool hasFP16Support(); static bool has1010102Support(); + static bool has10101010Support(); static bool hasAlpha8Support(); #else static bool hasFP16Support() { return true; } static bool has1010102Support() { return true; } + static bool has10101010Support() { return true; } static bool hasAlpha8Support() { return true; } #endif }; diff --git a/libs/hwui/Mesh.h b/libs/hwui/Mesh.h index 8c6ca9758479..afc6adf7f3e0 100644 --- a/libs/hwui/Mesh.h +++ b/libs/hwui/Mesh.h @@ -17,8 +17,8 @@ #ifndef MESH_H_ #define MESH_H_ -#include <GrDirectContext.h> #include <SkMesh.h> +#include <include/gpu/ganesh/GrDirectContext.h> #include <include/gpu/ganesh/SkMeshGanesh.h> #include <jni.h> #include <log/log.h> diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index d184f64b1c2c..b6476c9d466f 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -42,6 +42,14 @@ constexpr bool hdr_10bit_plus() { constexpr bool initialize_gl_always() { return false; } + +constexpr bool skip_eglmanager_telemetry() { + return false; +} + +constexpr bool resample_gainmap_regions() { + return false; +} } // namespace hwui_flags #endif @@ -100,6 +108,8 @@ float Properties::maxHdrHeadroomOn8bit = 5.f; // TODO: Refine this number bool Properties::clipSurfaceViews = false; bool Properties::hdr10bitPlus = false; +bool Properties::skipTelemetry = false; +bool Properties::resampleGainmapRegions = false; int Properties::timeoutMultiplier = 1; @@ -175,8 +185,12 @@ bool Properties::load() { clipSurfaceViews = base::GetBoolProperty("debug.hwui.clip_surfaceviews", hwui_flags::clip_surfaceviews()); hdr10bitPlus = hwui_flags::hdr_10bit_plus(); + resampleGainmapRegions = base::GetBoolProperty("debug.hwui.resample_gainmap_regions", + hwui_flags::resample_gainmap_regions()); timeoutMultiplier = android::base::GetIntProperty("ro.hw_timeout_multiplier", 1); + skipTelemetry = base::GetBoolProperty(PROPERTY_SKIP_EGLMANAGER_TELEMETRY, + hwui_flags::skip_eglmanager_telemetry()); return (prevDebugLayersUpdates != debugLayersUpdates) || (prevDebugOverdraw != debugOverdraw); } diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index e2646422030e..db471527b861 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -234,6 +234,8 @@ enum DebugLevel { */ #define PROPERTY_INITIALIZE_GL_ALWAYS "debug.hwui.initialize_gl_always" +#define PROPERTY_SKIP_EGLMANAGER_TELEMETRY "debug.hwui.skip_eglmanager_telemetry" + /////////////////////////////////////////////////////////////////////////////// // Misc /////////////////////////////////////////////////////////////////////////////// @@ -342,6 +344,8 @@ public: static bool clipSurfaceViews; static bool hdr10bitPlus; + static bool skipTelemetry; + static bool resampleGainmapRegions; static int timeoutMultiplier; diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp index d0263798d2c2..60d7f2d14708 100644 --- a/libs/hwui/RecordingCanvas.cpp +++ b/libs/hwui/RecordingCanvas.cpp @@ -16,9 +16,12 @@ #include "RecordingCanvas.h" -#include <GrRecordingContext.h> #include <SkMesh.h> #include <hwui/Paint.h> +#include <include/gpu/GpuTypes.h> +#include <include/gpu/ganesh/GrDirectContext.h> +#include <include/gpu/ganesh/GrRecordingContext.h> +#include <include/gpu/ganesh/SkMeshGanesh.h> #include <log/log.h> #include <experimental/type_traits> @@ -48,9 +51,6 @@ #include "Tonemapper.h" #include "VectorDrawable.h" #include "effects/GainmapRenderer.h" -#include "include/gpu/GpuTypes.h" // from Skia -#include "include/gpu/GrDirectContext.h" -#include "include/gpu/ganesh/SkMeshGanesh.h" #include "pipeline/skia/AnimatedDrawables.h" #include "pipeline/skia/FunctorDrawable.h" #ifdef __ANDROID__ diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp index 589abb4d87f4..2c23864317a4 100644 --- a/libs/hwui/RenderNode.cpp +++ b/libs/hwui/RenderNode.cpp @@ -404,13 +404,19 @@ void RenderNode::syncDisplayList(TreeObserver& observer, TreeInfo* info) { } } +inline bool RenderNode::isForceInvertDark(TreeInfo& info) { + return CC_UNLIKELY( + info.forceDarkType == android::uirenderer::ForceDarkType::FORCE_INVERT_COLOR_DARK); +} + inline bool RenderNode::shouldEnableForceDark(TreeInfo* info) { return CC_UNLIKELY( info && - (!info->disableForceDark || - info->forceDarkType == android::uirenderer::ForceDarkType::FORCE_INVERT_COLOR_DARK)); + (!info->disableForceDark || isForceInvertDark(*info))); } + + void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) { if (!shouldEnableForceDark(info)) { return; @@ -421,7 +427,7 @@ void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) { children.push_back(node); }); if (mDisplayList.hasText()) { - if (mDisplayList.hasFill()) { + if (isForceInvertDark(*info) && mDisplayList.hasFill()) { // Handle a special case for custom views that draw both text and background in the // same RenderNode, which would otherwise be altered to white-on-white text. usage = UsageHint::Container; diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h index c9045427bd42..afbbce7e27ee 100644 --- a/libs/hwui/RenderNode.h +++ b/libs/hwui/RenderNode.h @@ -234,6 +234,7 @@ private: void syncDisplayList(TreeObserver& observer, TreeInfo* info); void handleForceDark(TreeInfo* info); bool shouldEnableForceDark(TreeInfo* info); + bool isForceInvertDark(TreeInfo& info); void prepareTreeImpl(TreeObserver& observer, TreeInfo& info, bool functorsNeedLayer); void pushStagingPropertiesChanges(TreeInfo& info); diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp index 72e83afbd96f..9e825fb350d6 100644 --- a/libs/hwui/SkiaCanvas.cpp +++ b/libs/hwui/SkiaCanvas.cpp @@ -841,9 +841,6 @@ void SkiaCanvas::drawGlyphs(ReadGlyphFunc glyphFunc, int count, const Paint& pai sk_sp<SkTextBlob> textBlob(builder.make()); applyLooper(&paintCopy, [&](const SkPaint& p) { mCanvas->drawTextBlob(textBlob, 0, 0, p); }); - if (!text_feature::fix_double_underline()) { - drawTextDecorations(x, y, totalAdvance, paintCopy); - } } void SkiaCanvas::drawLayoutOnPath(const minikin::Layout& layout, float hOffset, float vOffset, diff --git a/libs/hwui/SkiaInterpolator.cpp b/libs/hwui/SkiaInterpolator.cpp index 5a45ad9085e7..8deac61e8baa 100644 --- a/libs/hwui/SkiaInterpolator.cpp +++ b/libs/hwui/SkiaInterpolator.cpp @@ -47,6 +47,8 @@ static inline Dot14 pin_and_convert(float x) { return static_cast<Dot14>(x * Dot14_ONE); } +using MSec = uint32_t; // millisecond duration + static float SkUnitCubicInterp(float value, float bx, float by, float cx, float cy) { // pin to the unit-square, and convert to 2.14 Dot14 x = pin_and_convert(value); @@ -120,7 +122,7 @@ void SkiaInterpolatorBase::reset(int elemCount, int frameCount) { Totaling fElemCount+2 entries per keyframe */ -bool SkiaInterpolatorBase::getDuration(SkMSec* startTime, SkMSec* endTime) const { +bool SkiaInterpolatorBase::getDuration(MSec* startTime, MSec* endTime) const { if (fFrameCount == 0) { return false; } @@ -134,7 +136,7 @@ bool SkiaInterpolatorBase::getDuration(SkMSec* startTime, SkMSec* endTime) const return true; } -float SkiaInterpolatorBase::ComputeRelativeT(SkMSec time, SkMSec prevTime, SkMSec nextTime, +float SkiaInterpolatorBase::ComputeRelativeT(MSec time, MSec prevTime, MSec nextTime, const float blend[4]) { LOG_FATAL_IF(time < prevTime || time > nextTime); @@ -144,7 +146,7 @@ float SkiaInterpolatorBase::ComputeRelativeT(SkMSec time, SkMSec prevTime, SkMSe // Returns the index of where the item is or the bit not of the index // where the item should go in order to keep arr sorted in ascending order. -int SkiaInterpolatorBase::binarySearch(const SkTimeCode* arr, int count, SkMSec target) { +int SkiaInterpolatorBase::binarySearch(const SkTimeCode* arr, int count, MSec target) { if (count <= 0) { return ~0; } @@ -154,7 +156,7 @@ int SkiaInterpolatorBase::binarySearch(const SkTimeCode* arr, int count, SkMSec while (lo < hi) { int mid = (hi + lo) / 2; - SkMSec elem = arr[mid].fTime; + MSec elem = arr[mid].fTime; if (elem == target) { return mid; } else if (elem < target) { @@ -171,21 +173,21 @@ int SkiaInterpolatorBase::binarySearch(const SkTimeCode* arr, int count, SkMSec return ~(lo + 1); } -SkiaInterpolatorBase::Result SkiaInterpolatorBase::timeToT(SkMSec time, float* T, int* indexPtr, +SkiaInterpolatorBase::Result SkiaInterpolatorBase::timeToT(MSec time, float* T, int* indexPtr, bool* exactPtr) const { LOG_FATAL_IF(fFrameCount <= 0); Result result = kNormal_Result; if (fRepeat != 1.0f) { - SkMSec startTime = 0, endTime = 0; // initialize to avoid warning + MSec startTime = 0, endTime = 0; // initialize to avoid warning this->getDuration(&startTime, &endTime); - SkMSec totalTime = endTime - startTime; - SkMSec offsetTime = time - startTime; + MSec totalTime = endTime - startTime; + MSec offsetTime = time - startTime; endTime = SkScalarFloorToInt(fRepeat * totalTime); if (offsetTime >= endTime) { float fraction = SkScalarFraction(fRepeat); offsetTime = fraction == 0 && fRepeat > 0 ? totalTime - : (SkMSec)SkScalarFloorToInt(fraction * totalTime); + : (MSec)SkScalarFloorToInt(fraction * totalTime); result = kFreezeEnd_Result; } else { int mirror = fFlags & kMirror; @@ -217,11 +219,11 @@ SkiaInterpolatorBase::Result SkiaInterpolatorBase::timeToT(SkMSec time, float* T } LOG_FATAL_IF(index >= fFrameCount); const SkTimeCode* nextTime = &fTimes[index]; - SkMSec nextT = nextTime[0].fTime; + MSec nextT = nextTime[0].fTime; if (exact) { *T = 0; } else { - SkMSec prevT = nextTime[-1].fTime; + MSec prevT = nextTime[-1].fTime; *T = ComputeRelativeT(time, prevT, nextT, nextTime[-1].fBlend); } *indexPtr = index; @@ -251,7 +253,7 @@ void SkiaInterpolator::reset(int elemCount, int frameCount) { static const float gIdentityBlend[4] = {0.33333333f, 0.33333333f, 0.66666667f, 0.66666667f}; -bool SkiaInterpolator::setKeyFrame(int index, SkMSec time, const float values[], +bool SkiaInterpolator::setKeyFrame(int index, MSec time, const float values[], const float blend[4]) { LOG_FATAL_IF(values == nullptr); @@ -272,7 +274,7 @@ bool SkiaInterpolator::setKeyFrame(int index, SkMSec time, const float values[], return success; } -SkiaInterpolator::Result SkiaInterpolator::timeToValues(SkMSec time, float values[]) const { +SkiaInterpolator::Result SkiaInterpolator::timeToValues(MSec time, float values[]) const { float T; int index; bool exact; diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index cd3ae5342f4e..ab052b902e02 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -2,6 +2,13 @@ package: "com.android.graphics.hwui.flags" container: "system" flag { + name: "runtime_color_filters_blenders" + namespace: "core_graphics" + description: "API for AGSL authored runtime color filters and blenders" + bug: "358126864" +} + +flag { name: "clip_shader" is_exported: true namespace: "core_graphics" @@ -97,3 +104,28 @@ flag { description: "Initialize GL even when HWUI is set to use Vulkan. This improves app startup time for apps using GL." bug: "335172671" } + +flag { + name: "skip_eglmanager_telemetry" + namespace: "core_graphics" + description: "Skip telemetry in EglManager's calls to eglCreateContext to avoid polluting telemetry" + bug: "347911216" +} + +flag { + name: "resample_gainmap_regions" + namespace: "core_graphics" + description: "Resample gainmaps when decoding regions, to improve visual quality" + bug: "352847821" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "iso_gainmap_apis" + is_exported: true + namespace: "core_graphics" + description: "APIs that expose gainmap metadata corresponding to those defined in ISO 21496-1" + bug: "349357636" +} diff --git a/libs/hwui/apex/LayoutlibLoader.cpp b/libs/hwui/apex/LayoutlibLoader.cpp index 70a9ef04d6f3..b4e6b7243ddc 100644 --- a/libs/hwui/apex/LayoutlibLoader.cpp +++ b/libs/hwui/apex/LayoutlibLoader.cpp @@ -28,6 +28,7 @@ using namespace std; extern int register_android_graphics_Bitmap(JNIEnv*); extern int register_android_graphics_BitmapFactory(JNIEnv*); +extern int register_android_graphics_BitmapRegionDecoder(JNIEnv*); extern int register_android_graphics_ByteBufferStreamAdaptor(JNIEnv* env); extern int register_android_graphics_Camera(JNIEnv* env); extern int register_android_graphics_CreateJavaOutputStreamAdaptor(JNIEnv* env); @@ -41,6 +42,7 @@ extern int register_android_graphics_Shader(JNIEnv* env); extern int register_android_graphics_RenderEffect(JNIEnv* env); extern int register_android_graphics_Typeface(JNIEnv* env); extern int register_android_graphics_YuvImage(JNIEnv* env); +extern int register_android_graphics_drawable_AnimatedImageDrawable(JNIEnv* env); namespace android { @@ -51,7 +53,12 @@ extern int register_android_graphics_ColorFilter(JNIEnv* env); extern int register_android_graphics_ColorSpace(JNIEnv* env); extern int register_android_graphics_DrawFilter(JNIEnv* env); extern int register_android_graphics_FontFamily(JNIEnv* env); +extern int register_android_graphics_Gainmap(JNIEnv* env); +extern int register_android_graphics_HardwareBufferRenderer(JNIEnv* env); +extern int register_android_graphics_HardwareRendererObserver(JNIEnv* env); extern int register_android_graphics_Matrix(JNIEnv* env); +extern int register_android_graphics_Mesh(JNIEnv* env); +extern int register_android_graphics_MeshSpecification(JNIEnv* env); extern int register_android_graphics_Paint(JNIEnv* env); extern int register_android_graphics_Path(JNIEnv* env); extern int register_android_graphics_PathIterator(JNIEnv* env); @@ -72,6 +79,7 @@ extern int register_android_graphics_text_GraphemeBreak(JNIEnv* env); extern int register_android_util_PathParser(JNIEnv* env); extern int register_android_view_DisplayListCanvas(JNIEnv* env); extern int register_android_view_RenderNode(JNIEnv* env); +extern int register_android_view_ThreadedRenderer(JNIEnv* env); #define REG_JNI(name) { name } struct RegJNIRec { @@ -83,6 +91,8 @@ struct RegJNIRec { static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { {"android.graphics.Bitmap", REG_JNI(register_android_graphics_Bitmap)}, {"android.graphics.BitmapFactory", REG_JNI(register_android_graphics_BitmapFactory)}, + {"android.graphics.BitmapRegionDecoder", + REG_JNI(register_android_graphics_BitmapRegionDecoder)}, {"android.graphics.ByteBufferStreamAdaptor", REG_JNI(register_android_graphics_ByteBufferStreamAdaptor)}, {"android.graphics.Camera", REG_JNI(register_android_graphics_Camera)}, @@ -95,11 +105,20 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { REG_JNI(register_android_graphics_CreateJavaOutputStreamAdaptor)}, {"android.graphics.DrawFilter", REG_JNI(register_android_graphics_DrawFilter)}, {"android.graphics.FontFamily", REG_JNI(register_android_graphics_FontFamily)}, + {"android.graphics.Gainmap", REG_JNI(register_android_graphics_Gainmap)}, {"android.graphics.Graphics", REG_JNI(register_android_graphics_Graphics)}, + {"android.graphics.HardwareBufferRenderer", + REG_JNI(register_android_graphics_HardwareBufferRenderer)}, + {"android.graphics.HardwareRenderer", REG_JNI(register_android_view_ThreadedRenderer)}, + {"android.graphics.HardwareRendererObserver", + REG_JNI(register_android_graphics_HardwareRendererObserver)}, {"android.graphics.ImageDecoder", REG_JNI(register_android_graphics_ImageDecoder)}, {"android.graphics.Interpolator", REG_JNI(register_android_graphics_Interpolator)}, {"android.graphics.MaskFilter", REG_JNI(register_android_graphics_MaskFilter)}, {"android.graphics.Matrix", REG_JNI(register_android_graphics_Matrix)}, + {"android.graphics.Mesh", REG_JNI(register_android_graphics_Mesh)}, + {"android.graphics.MeshSpecification", + REG_JNI(register_android_graphics_MeshSpecification)}, {"android.graphics.NinePatch", REG_JNI(register_android_graphics_NinePatch)}, {"android.graphics.Paint", REG_JNI(register_android_graphics_Paint)}, {"android.graphics.Path", REG_JNI(register_android_graphics_Path)}, @@ -118,6 +137,8 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { REG_JNI(register_android_graphics_animation_NativeInterpolatorFactory)}, {"android.graphics.animation.RenderNodeAnimator", REG_JNI(register_android_graphics_animation_RenderNodeAnimator)}, + {"android.graphics.drawable.AnimatedImageDrawable", + REG_JNI(register_android_graphics_drawable_AnimatedImageDrawable)}, {"android.graphics.drawable.AnimatedVectorDrawable", REG_JNI(register_android_graphics_drawable_AnimatedVectorDrawable)}, {"android.graphics.drawable.VectorDrawable", diff --git a/libs/hwui/apex/android_bitmap.cpp b/libs/hwui/apex/android_bitmap.cpp index c80a9b4ae97f..000f1092eb17 100644 --- a/libs/hwui/apex/android_bitmap.cpp +++ b/libs/hwui/apex/android_bitmap.cpp @@ -14,21 +14,21 @@ * limitations under the License. */ -#include <log/log.h> - -#include "android/graphics/bitmap.h" -#include "TypeCast.h" -#include "GraphicsJNI.h" - +#include <Gainmap.h> #include <GraphicsJNI.h> -#include <hwui/Bitmap.h> #include <SkBitmap.h> #include <SkColorSpace.h> #include <SkImageInfo.h> #include <SkRefCnt.h> #include <SkStream.h> +#include <hwui/Bitmap.h> +#include <log/log.h> #include <utils/Color.h> +#include "GraphicsJNI.h" +#include "TypeCast.h" +#include "android/graphics/bitmap.h" + using namespace android; ABitmap* ABitmap_acquireBitmapFromJava(JNIEnv* env, jobject bitmapObj) { @@ -215,6 +215,14 @@ private: int ABitmap_compress(const AndroidBitmapInfo* info, ADataSpace dataSpace, const void* pixels, AndroidBitmapCompressFormat inFormat, int32_t quality, void* userContext, AndroidBitmap_CompressWriteFunc fn) { + return ABitmap_compressWithGainmap(info, dataSpace, pixels, nullptr, -1.f, inFormat, quality, + userContext, fn); +} + +int ABitmap_compressWithGainmap(const AndroidBitmapInfo* info, ADataSpace dataSpace, + const void* pixels, const void* gainmapPixels, float hdrSdrRatio, + AndroidBitmapCompressFormat inFormat, int32_t quality, + void* userContext, AndroidBitmap_CompressWriteFunc fn) { Bitmap::JavaCompressFormat format; switch (inFormat) { case ANDROID_BITMAP_COMPRESS_FORMAT_JPEG: @@ -275,7 +283,7 @@ int ABitmap_compress(const AndroidBitmapInfo* info, ADataSpace dataSpace, const // besides ADATASPACE_UNKNOWN as an error? cs = nullptr; } else { - cs = uirenderer::DataSpaceToColorSpace((android_dataspace) dataSpace); + cs = uirenderer::DataSpaceToColorSpace((android_dataspace)dataSpace); // DataSpaceToColorSpace treats UNKNOWN as SRGB, but compress forces the // client to specify SRGB if that is what they want. if (!cs || dataSpace == ADATASPACE_UNKNOWN) { @@ -292,16 +300,70 @@ int ABitmap_compress(const AndroidBitmapInfo* info, ADataSpace dataSpace, const auto imageInfo = SkImageInfo::Make(info->width, info->height, colorType, alphaType, std::move(cs)); - SkBitmap bitmap; - // We are not going to modify the pixels, but installPixels expects them to - // not be const, since for all it knows we might want to draw to the SkBitmap. - if (!bitmap.installPixels(imageInfo, const_cast<void*>(pixels), info->stride)) { - return ANDROID_BITMAP_RESULT_BAD_PARAMETER; + + // Validate the image info + { + SkBitmap tempBitmap; + if (!tempBitmap.installPixels(imageInfo, const_cast<void*>(pixels), info->stride)) { + return ANDROID_BITMAP_RESULT_BAD_PARAMETER; + } + } + + SkPixelRef pixelRef = + SkPixelRef(info->width, info->height, const_cast<void*>(pixels), info->stride); + + auto bitmap = Bitmap::createFrom(imageInfo, pixelRef); + + if (gainmapPixels) { + auto gainmap = sp<uirenderer::Gainmap>::make(); + gainmap->info.fGainmapRatioMin = { + 1.f, + 1.f, + 1.f, + 1.f, + }; + gainmap->info.fGainmapRatioMax = { + hdrSdrRatio, + hdrSdrRatio, + hdrSdrRatio, + 1.f, + }; + gainmap->info.fGainmapGamma = { + 1.f, + 1.f, + 1.f, + 1.f, + }; + + static constexpr auto kDefaultEpsilon = 1.f / 64.f; + gainmap->info.fEpsilonSdr = { + kDefaultEpsilon, + kDefaultEpsilon, + kDefaultEpsilon, + 1.f, + }; + gainmap->info.fEpsilonHdr = { + kDefaultEpsilon, + kDefaultEpsilon, + kDefaultEpsilon, + 1.f, + }; + gainmap->info.fDisplayRatioSdr = 1.f; + gainmap->info.fDisplayRatioHdr = hdrSdrRatio; + + SkPixelRef gainmapPixelRef = SkPixelRef(info->width, info->height, + const_cast<void*>(gainmapPixels), info->stride); + auto gainmapBitmap = Bitmap::createFrom(imageInfo, gainmapPixelRef); + gainmap->bitmap = std::move(gainmapBitmap); + bitmap->setGainmap(std::move(gainmap)); } CompressWriter stream(userContext, fn); - return Bitmap::compress(bitmap, format, quality, &stream) ? ANDROID_BITMAP_RESULT_SUCCESS - : ANDROID_BITMAP_RESULT_JNI_EXCEPTION; + + return bitmap->compress(format, quality, &stream) + + ? ANDROID_BITMAP_RESULT_SUCCESS + : ANDROID_BITMAP_RESULT_JNI_EXCEPTION; } AHardwareBuffer* ABitmap_getHardwareBuffer(ABitmap* bitmapHandle) { diff --git a/libs/hwui/apex/include/android/graphics/bitmap.h b/libs/hwui/apex/include/android/graphics/bitmap.h index 8c4b439d2a2b..6f65e9eb0495 100644 --- a/libs/hwui/apex/include/android/graphics/bitmap.h +++ b/libs/hwui/apex/include/android/graphics/bitmap.h @@ -65,6 +65,13 @@ ANDROID_API jobject ABitmapConfig_getConfigFromFormat(JNIEnv* env, AndroidBitmap ANDROID_API int ABitmap_compress(const AndroidBitmapInfo* info, ADataSpace dataSpace, const void* pixels, AndroidBitmapCompressFormat format, int32_t quality, void* userContext, AndroidBitmap_CompressWriteFunc); +// If gainmapPixels is null, then no gainmap is encoded, and hdrSdrRatio is +// unused +ANDROID_API int ABitmap_compressWithGainmap(const AndroidBitmapInfo* info, ADataSpace dataSpace, + const void* pixels, const void* gainmapPixels, + float hdrSdrRatio, AndroidBitmapCompressFormat format, + int32_t quality, void* userContext, + AndroidBitmap_CompressWriteFunc); /** * Retrieve the native object associated with a HARDWARE Bitmap. * diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp index 185436160349..84bd45dfc012 100644 --- a/libs/hwui/hwui/Bitmap.cpp +++ b/libs/hwui/hwui/Bitmap.cpp @@ -264,6 +264,7 @@ Bitmap::Bitmap(void* address, size_t size, const SkImageInfo& info, size_t rowBy , mPixelStorageType(PixelStorageType::Heap) { mPixelStorage.heap.address = address; mPixelStorage.heap.size = size; + traceBitmapCreate(); } Bitmap::Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info) @@ -272,6 +273,7 @@ Bitmap::Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info) , mPixelStorageType(PixelStorageType::WrappedPixelRef) { pixelRef.ref(); mPixelStorage.wrapped.pixelRef = &pixelRef; + traceBitmapCreate(); } Bitmap::Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes) @@ -281,6 +283,7 @@ Bitmap::Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info mPixelStorage.ashmem.address = address; mPixelStorage.ashmem.fd = fd; mPixelStorage.ashmem.size = mappedSize; + traceBitmapCreate(); } #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration @@ -297,10 +300,12 @@ Bitmap::Bitmap(AHardwareBuffer* buffer, const SkImageInfo& info, size_t rowBytes setImmutable(); // HW bitmaps are always immutable mImage = SkImages::DeferredFromAHardwareBuffer(buffer, mInfo.alphaType(), mInfo.refColorSpace()); + traceBitmapCreate(); } #endif Bitmap::~Bitmap() { + traceBitmapDelete(); switch (mPixelStorageType) { case PixelStorageType::WrappedPixelRef: mPixelStorage.wrapped.pixelRef->unref(); @@ -572,4 +577,28 @@ void Bitmap::setGainmap(sp<uirenderer::Gainmap>&& gainmap) { mGainmap = std::move(gainmap); } +std::mutex Bitmap::mLock{}; +size_t Bitmap::mTotalBitmapBytes = 0; +size_t Bitmap::mTotalBitmapCount = 0; + +void Bitmap::traceBitmapCreate() { + if (ATRACE_ENABLED()) { + std::lock_guard lock{mLock}; + mTotalBitmapBytes += getAllocationByteCount(); + mTotalBitmapCount++; + ATRACE_INT64("Bitmap Memory", mTotalBitmapBytes); + ATRACE_INT64("Bitmap Count", mTotalBitmapCount); + } +} + +void Bitmap::traceBitmapDelete() { + if (ATRACE_ENABLED()) { + std::lock_guard lock{mLock}; + mTotalBitmapBytes -= getAllocationByteCount(); + mTotalBitmapCount--; + ATRACE_INT64("Bitmap Memory", mTotalBitmapBytes); + ATRACE_INT64("Bitmap Count", mTotalBitmapCount); + } +} + } // namespace android diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h index dd344e2f5517..3d55d859ed5f 100644 --- a/libs/hwui/hwui/Bitmap.h +++ b/libs/hwui/hwui/Bitmap.h @@ -25,6 +25,7 @@ #include <cutils/compiler.h> #include <utils/StrongPointer.h> +#include <mutex> #include <optional> #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration @@ -227,6 +228,13 @@ private: } mPixelStorage; sk_sp<SkImage> mImage; // Cache is used only for HW Bitmaps with Skia pipeline. + + // for tracing total number and memory usage of bitmaps + static std::mutex mLock; + static size_t mTotalBitmapBytes; + static size_t mTotalBitmapCount; + void traceBitmapCreate(); + void traceBitmapDelete(); }; } // namespace android diff --git a/libs/hwui/hwui/Canvas.cpp b/libs/hwui/hwui/Canvas.cpp index 80b6c0385fca..5af4af27babd 100644 --- a/libs/hwui/hwui/Canvas.cpp +++ b/libs/hwui/hwui/Canvas.cpp @@ -110,28 +110,26 @@ void Canvas::drawText(const uint16_t* text, int textSize, int start, int count, DrawTextFunctor f(layout, this, paint, x, y, layout.getAdvance()); MinikinUtils::forFontRun(layout, &paint, f); - if (text_feature::fix_double_underline()) { - Paint copied(paint); - PaintFilter* filter = getPaintFilter(); - if (filter != nullptr) { - filter->filterFullPaint(&copied); + Paint copied(paint); + PaintFilter* filter = getPaintFilter(); + if (filter != nullptr) { + filter->filterFullPaint(&copied); + } + const bool isUnderline = copied.isUnderline(); + const bool isStrikeThru = copied.isStrikeThru(); + if (isUnderline || isStrikeThru) { + const SkScalar left = x; + const SkScalar right = x + layout.getAdvance(); + if (isUnderline) { + const SkScalar top = y + f.getUnderlinePosition(); + drawStroke(left, right, top, f.getUnderlineThickness(), copied, this); } - const bool isUnderline = copied.isUnderline(); - const bool isStrikeThru = copied.isStrikeThru(); - if (isUnderline || isStrikeThru) { - const SkScalar left = x; - const SkScalar right = x + layout.getAdvance(); - if (isUnderline) { - const SkScalar top = y + f.getUnderlinePosition(); - drawStroke(left, right, top, f.getUnderlineThickness(), copied, this); - } - if (isStrikeThru) { - float textSize = paint.getSkFont().getSize(); - const float position = textSize * Paint::kStdStrikeThru_Top; - const SkScalar thickness = textSize * Paint::kStdStrikeThru_Thickness; - const SkScalar top = y + position; - drawStroke(left, right, top, thickness, copied, this); - } + if (isStrikeThru) { + float textSize = paint.getSkFont().getSize(); + const float position = textSize * Paint::kStdStrikeThru_Top; + const SkScalar thickness = textSize * Paint::kStdStrikeThru_Thickness; + const SkScalar top = y + position; + drawStroke(left, right, top, thickness, copied, this); } } } diff --git a/libs/hwui/hwui/Canvas.h b/libs/hwui/hwui/Canvas.h index 4eb6918d7e9a..b6988b21333b 100644 --- a/libs/hwui/hwui/Canvas.h +++ b/libs/hwui/hwui/Canvas.h @@ -28,11 +28,9 @@ #include "pipeline/skia/AnimatedDrawables.h" #include "utils/Macros.h" -class SkAnimatedImage; enum class SkBlendMode; class SkCanvasState; class SkRRect; -class SkRuntimeShaderBuilder; class SkVertices; namespace minikin { diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h index d264058737e5..e13e136550ca 100644 --- a/libs/hwui/hwui/DrawTextFunctor.h +++ b/libs/hwui/hwui/DrawTextFunctor.h @@ -17,7 +17,6 @@ #include <SkFontMetrics.h> #include <SkRRect.h> #include <SkTextBlob.h> -#include <com_android_graphics_hwui_flags.h> #include "../utils/Color.h" #include "Canvas.h" @@ -30,7 +29,19 @@ #include "hwui/PaintFilter.h" #include "pipeline/skia/SkiaRecordingCanvas.h" +#ifdef __ANDROID__ +#include <com_android_graphics_hwui_flags.h> namespace flags = com::android::graphics::hwui::flags; +#else +namespace flags { +constexpr bool high_contrast_text_luminance() { + return false; +} +constexpr bool high_contrast_text_small_text_rect() { + return false; +} +} // namespace flags +#endif namespace android { @@ -132,32 +143,30 @@ public: canvas->drawGlyphs(glyphFunc, glyphCount, paint, x, y, totalAdvance); } - if (text_feature::fix_double_underline()) { - // Extract underline position and thickness. - if (paint.isUnderline()) { - SkFontMetrics metrics; - paint.getSkFont().getMetrics(&metrics); - const float textSize = paint.getSkFont().getSize(); - SkScalar position; - if (!metrics.hasUnderlinePosition(&position)) { - position = textSize * Paint::kStdUnderline_Top; - } - SkScalar thickness; - if (!metrics.hasUnderlineThickness(&thickness)) { - thickness = textSize * Paint::kStdUnderline_Thickness; - } - - // If multiple fonts are used, use the most bottom position and most thick stroke - // width as the underline position. This follows the CSS standard: - // https://www.w3.org/TR/css-text-decor-3/#text-underline-position-property - // <quote> - // The exact position and thickness of line decorations is UA-defined in this level. - // However, for underlines and overlines the UA must use a single thickness and - // position on each line for the decorations deriving from a single decorating box. - // </quote> - underlinePosition = std::max(underlinePosition, position); - underlineThickness = std::max(underlineThickness, thickness); + // Extract underline position and thickness. + if (paint.isUnderline()) { + SkFontMetrics metrics; + paint.getSkFont().getMetrics(&metrics); + const float textSize = paint.getSkFont().getSize(); + SkScalar position; + if (!metrics.hasUnderlinePosition(&position)) { + position = textSize * Paint::kStdUnderline_Top; + } + SkScalar thickness; + if (!metrics.hasUnderlineThickness(&thickness)) { + thickness = textSize * Paint::kStdUnderline_Thickness; } + + // If multiple fonts are used, use the most bottom position and most thick stroke + // width as the underline position. This follows the CSS standard: + // https://www.w3.org/TR/css-text-decor-3/#text-underline-position-property + // <quote> + // The exact position and thickness of line decorations is UA-defined in this level. + // However, for underlines and overlines the UA must use a single thickness and + // position on each line for the decorations deriving from a single decorating box. + // </quote> + underlinePosition = std::max(underlinePosition, position); + underlineThickness = std::max(underlineThickness, thickness); } } diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp index d66d7f8e83f4..ede385adc779 100644 --- a/libs/hwui/hwui/MinikinUtils.cpp +++ b/libs/hwui/hwui/MinikinUtils.cpp @@ -53,9 +53,7 @@ minikin::MinikinPaint MinikinUtils::prepareMinikinPaint(const Paint* paint, if (familyVariant.has_value()) { minikinPaint.familyVariant = familyVariant.value(); } else { - minikinPaint.familyVariant = text_feature::deprecate_ui_fonts() - ? minikin::FamilyVariant::ELEGANT - : minikin::FamilyVariant::DEFAULT; + minikinPaint.familyVariant = minikin::FamilyVariant::ELEGANT; } return minikinPaint; } diff --git a/libs/hwui/hwui/MinikinUtils.h b/libs/hwui/hwui/MinikinUtils.h index f8574ee50525..1510ce1378d8 100644 --- a/libs/hwui/hwui/MinikinUtils.h +++ b/libs/hwui/hwui/MinikinUtils.h @@ -27,6 +27,8 @@ #include <cutils/compiler.h> #include <log/log.h> #include <minikin/Layout.h> + +#include "FeatureFlags.h" #include "MinikinSkia.h" #include "Paint.h" #include "Typeface.h" @@ -71,27 +73,42 @@ public: static void forFontRun(const minikin::Layout& layout, Paint* paint, F& f) { float saveSkewX = paint->getSkFont().getSkewX(); bool savefakeBold = paint->getSkFont().isEmbolden(); - const minikin::MinikinFont* curFont = nullptr; - size_t start = 0; - size_t nGlyphs = layout.nGlyphs(); - for (size_t i = 0; i < nGlyphs; i++) { - const minikin::MinikinFont* nextFont = layout.typeface(i).get(); - if (i > 0 && nextFont != curFont) { + if (text_feature::typeface_redesign()) { + for (uint32_t runIdx = 0; runIdx < layout.getFontRunCount(); ++runIdx) { + uint32_t start = layout.getFontRunStart(runIdx); + uint32_t end = layout.getFontRunEnd(runIdx); + const minikin::FakedFont& fakedFont = layout.getFontRunFont(runIdx); + + std::shared_ptr<minikin::MinikinFont> font = fakedFont.typeface(); + SkFont* skfont = &paint->getSkFont(); + MinikinFontSkia::populateSkFont(skfont, font.get(), fakedFont.fakery); + f(start, end); + skfont->setSkewX(saveSkewX); + skfont->setEmbolden(savefakeBold); + } + } else { + const minikin::MinikinFont* curFont = nullptr; + size_t start = 0; + size_t nGlyphs = layout.nGlyphs(); + for (size_t i = 0; i < nGlyphs; i++) { + const minikin::MinikinFont* nextFont = layout.typeface(i).get(); + if (i > 0 && nextFont != curFont) { + SkFont* skfont = &paint->getSkFont(); + MinikinFontSkia::populateSkFont(skfont, curFont, layout.getFakery(start)); + f(start, i); + skfont->setSkewX(saveSkewX); + skfont->setEmbolden(savefakeBold); + start = i; + } + curFont = nextFont; + } + if (nGlyphs > start) { SkFont* skfont = &paint->getSkFont(); MinikinFontSkia::populateSkFont(skfont, curFont, layout.getFakery(start)); - f(start, i); + f(start, nGlyphs); skfont->setSkewX(saveSkewX); skfont->setEmbolden(savefakeBold); - start = i; } - curFont = nextFont; - } - if (nGlyphs > start) { - SkFont* skfont = &paint->getSkFont(); - MinikinFontSkia::populateSkFont(skfont, curFont, layout.getFakery(start)); - f(start, nGlyphs); - skfont->setSkewX(saveSkewX); - skfont->setEmbolden(savefakeBold); } } }; diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp index d4157008ca46..010c4e8dfb3a 100644 --- a/libs/hwui/jni/Bitmap.cpp +++ b/libs/hwui/jni/Bitmap.cpp @@ -33,9 +33,6 @@ #define DEBUG_PARCEL 0 static jclass gBitmap_class; -static jfieldID gBitmap_nativePtr; -static jmethodID gBitmap_constructorMethodID; -static jmethodID gBitmap_reinitMethodID; namespace android { @@ -183,6 +180,9 @@ static void assert_premultiplied(const SkImageInfo& info, bool isPremultiplied) void reinitBitmap(JNIEnv* env, jobject javaBitmap, const SkImageInfo& info, bool isPremultiplied) { + static jmethodID gBitmap_reinitMethodID = + GetMethodIDOrDie(env, gBitmap_class, "reinit", "(IIZ)V"); + // The caller needs to have already set the alpha type properly, so the // native SkBitmap stays in sync with the Java Bitmap. assert_premultiplied(info, isPremultiplied); @@ -194,6 +194,10 @@ void reinitBitmap(JNIEnv* env, jobject javaBitmap, const SkImageInfo& info, jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets, int density) { + static jmethodID gBitmap_constructorMethodID = + GetMethodIDOrDie(env, gBitmap_class, + "<init>", "(JIIIZ[BLandroid/graphics/NinePatch$InsetStruct;Z)V"); + bool isMutable = bitmapCreateFlags & kBitmapCreateFlag_Mutable; bool isPremultiplied = bitmapCreateFlags & kBitmapCreateFlag_Premultiplied; // The caller needs to have already set the alpha type properly, so the @@ -232,11 +236,17 @@ Bitmap& toBitmap(jlong bitmapHandle) { using namespace android; using namespace android::bitmap; +static inline jlong getNativePtr(JNIEnv* env, jobject bitmap) { + static jfieldID gBitmap_nativePtr = + GetFieldIDOrDie(env, gBitmap_class, "mNativePtr", "J"); + return env->GetLongField(bitmap, gBitmap_nativePtr); +} + Bitmap* GraphicsJNI::getNativeBitmap(JNIEnv* env, jobject bitmap) { SkASSERT(env); SkASSERT(bitmap); SkASSERT(env->IsInstanceOf(bitmap, gBitmap_class)); - jlong bitmapHandle = env->GetLongField(bitmap, gBitmap_nativePtr); + jlong bitmapHandle = getNativePtr(env, bitmap); LocalScopedBitmap localBitmap(bitmapHandle); return localBitmap.valid() ? &localBitmap->bitmap() : nullptr; } @@ -246,7 +256,7 @@ SkImageInfo GraphicsJNI::getBitmapInfo(JNIEnv* env, jobject bitmap, uint32_t* ou SkASSERT(env); SkASSERT(bitmap); SkASSERT(env->IsInstanceOf(bitmap, gBitmap_class)); - jlong bitmapHandle = env->GetLongField(bitmap, gBitmap_nativePtr); + jlong bitmapHandle = getNativePtr(env, bitmap); LocalScopedBitmap localBitmap(bitmapHandle); if (outRowBytes) { *outRowBytes = localBitmap->rowBytes(); @@ -1269,9 +1279,6 @@ static const JNINativeMethod gBitmapMethods[] = { int register_android_graphics_Bitmap(JNIEnv* env) { gBitmap_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Bitmap")); - gBitmap_nativePtr = GetFieldIDOrDie(env, gBitmap_class, "mNativePtr", "J"); - gBitmap_constructorMethodID = GetMethodIDOrDie(env, gBitmap_class, "<init>", "(JIIIZ[BLandroid/graphics/NinePatch$InsetStruct;Z)V"); - gBitmap_reinitMethodID = GetMethodIDOrDie(env, gBitmap_class, "reinit", "(IIZ)V"); uirenderer::HardwareBufferHelpers::init(); return android::RegisterMethodsOrDie(env, "android/graphics/Bitmap", gBitmapMethods, NELEM(gBitmapMethods)); diff --git a/libs/hwui/jni/BitmapRegionDecoder.cpp b/libs/hwui/jni/BitmapRegionDecoder.cpp index ea5c14486ea4..6a65b8273194 100644 --- a/libs/hwui/jni/BitmapRegionDecoder.cpp +++ b/libs/hwui/jni/BitmapRegionDecoder.cpp @@ -87,8 +87,17 @@ public: requireUnpremul, prefColorSpace); } - bool decodeGainmapRegion(sp<uirenderer::Gainmap>* outGainmap, int outWidth, int outHeight, - const SkIRect& desiredSubset, int sampleSize, bool requireUnpremul) { + // Decodes the gainmap region. If decoding succeeded, returns true and + // populate outGainmap with the decoded gainmap. Otherwise, returns false. + // + // Note that the desiredSubset is the logical region within the source + // gainmap that we want to decode. This is used for scaling into the final + // bitmap, since we do not want to include portions of the gainmap outside + // of this region. desiredSubset is also _not_ guaranteed to be + // pixel-aligned, so it's not possible to simply resize the resulting + // bitmap to accomplish this. + bool decodeGainmapRegion(sp<uirenderer::Gainmap>* outGainmap, SkISize bitmapDimensions, + const SkRect& desiredSubset, int sampleSize, bool requireUnpremul) { SkColorType decodeColorType = mGainmapBRD->computeOutputColorType(kN32_SkColorType); sk_sp<SkColorSpace> decodeColorSpace = mGainmapBRD->computeOutputColorSpace(decodeColorType, nullptr); @@ -107,13 +116,30 @@ public: // allocation type. RecyclingClippingPixelAllocator will populate this with the // actual alpha type in either allocPixelRef() or copyIfNecessary() sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(SkImageInfo::Make( - outWidth, outHeight, decodeColorType, kPremul_SkAlphaType, decodeColorSpace)); + bitmapDimensions, decodeColorType, kPremul_SkAlphaType, decodeColorSpace)); if (!nativeBitmap) { ALOGE("OOM allocating Bitmap for Gainmap"); return false; } - RecyclingClippingPixelAllocator allocator(nativeBitmap.get(), false); - if (!mGainmapBRD->decodeRegion(&bm, &allocator, desiredSubset, sampleSize, decodeColorType, + + // Round out the subset so that we decode a slightly larger region, in + // case the subset has fractional components. + SkIRect roundedSubset = desiredSubset.roundOut(); + + // Map the desired subset to the space of the decoded gainmap. The + // subset is repositioned relative to the resulting bitmap, and then + // scaled to respect the sampleSize. + // This assumes that the subset will not be modified by the decoder, which is true + // for existing gainmap formats. + SkRect logicalSubset = desiredSubset.makeOffset(-std::floorf(desiredSubset.left()), + -std::floorf(desiredSubset.top())); + logicalSubset.fLeft /= sampleSize; + logicalSubset.fTop /= sampleSize; + logicalSubset.fRight /= sampleSize; + logicalSubset.fBottom /= sampleSize; + + RecyclingClippingPixelAllocator allocator(nativeBitmap.get(), false, logicalSubset); + if (!mGainmapBRD->decodeRegion(&bm, &allocator, roundedSubset, sampleSize, decodeColorType, requireUnpremul, decodeColorSpace)) { ALOGE("Error decoding Gainmap region"); return false; @@ -130,16 +156,31 @@ public: return true; } - SkIRect calculateGainmapRegion(const SkIRect& mainImageRegion, int* inOutWidth, - int* inOutHeight) { + struct Projection { + SkRect srcRect; + SkISize destSize; + }; + Projection calculateGainmapRegion(const SkIRect& mainImageRegion, SkISize dimensions) { const float scaleX = ((float)mGainmapBRD->width()) / mMainImageBRD->width(); const float scaleY = ((float)mGainmapBRD->height()) / mMainImageBRD->height(); - *inOutWidth *= scaleX; - *inOutHeight *= scaleY; - // TODO: Account for rounding error? - return SkIRect::MakeLTRB(mainImageRegion.left() * scaleX, mainImageRegion.top() * scaleY, - mainImageRegion.right() * scaleX, - mainImageRegion.bottom() * scaleY); + + if (uirenderer::Properties::resampleGainmapRegions) { + const auto srcRect = SkRect::MakeLTRB( + mainImageRegion.left() * scaleX, mainImageRegion.top() * scaleY, + mainImageRegion.right() * scaleX, mainImageRegion.bottom() * scaleY); + // Request a slightly larger destination size so that the gainmap + // subset we want fits entirely in this size. + const auto destSize = SkISize::Make(std::ceil(dimensions.width() * scaleX), + std::ceil(dimensions.height() * scaleY)); + return Projection{.srcRect = srcRect, .destSize = destSize}; + } else { + const auto srcRect = SkRect::Make(SkIRect::MakeLTRB( + mainImageRegion.left() * scaleX, mainImageRegion.top() * scaleY, + mainImageRegion.right() * scaleX, mainImageRegion.bottom() * scaleY)); + const auto destSize = + SkISize::Make(dimensions.width() * scaleX, dimensions.height() * scaleY); + return Projection{.srcRect = srcRect, .destSize = destSize}; + } } bool hasGainmap() { return mGainmapBRD != nullptr; } @@ -327,16 +368,16 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in sp<uirenderer::Gainmap> gainmap; bool hasGainmap = brd->hasGainmap(); if (hasGainmap) { - int gainmapWidth = bitmap.width(); - int gainmapHeight = bitmap.height(); + SkISize gainmapDims = SkISize::Make(bitmap.width(), bitmap.height()); if (javaBitmap) { // If we are recycling we must match the inBitmap's relative dimensions - gainmapWidth = recycledBitmap->width(); - gainmapHeight = recycledBitmap->height(); + gainmapDims.fWidth = recycledBitmap->width(); + gainmapDims.fHeight = recycledBitmap->height(); } - SkIRect gainmapSubset = brd->calculateGainmapRegion(subset, &gainmapWidth, &gainmapHeight); - if (!brd->decodeGainmapRegion(&gainmap, gainmapWidth, gainmapHeight, gainmapSubset, - sampleSize, requireUnpremul)) { + BitmapRegionDecoderWrapper::Projection gainmapProjection = + brd->calculateGainmapRegion(subset, gainmapDims); + if (!brd->decodeGainmapRegion(&gainmap, gainmapProjection.destSize, + gainmapProjection.srcRect, sampleSize, requireUnpremul)) { // If there is an error decoding Gainmap - we don't fail. We just don't provide Gainmap hasGainmap = false; } diff --git a/libs/hwui/jni/GIFMovie.cpp b/libs/hwui/jni/GIFMovie.cpp index ae6ac4ce4ecc..6c82aa1ca27d 100644 --- a/libs/hwui/jni/GIFMovie.cpp +++ b/libs/hwui/jni/GIFMovie.cpp @@ -30,7 +30,7 @@ public: protected: virtual bool onGetInfo(Info*); - virtual bool onSetTime(SkMSec); + virtual bool onSetTime(Movie::MSec); virtual bool onGetBitmap(SkBitmap*); private: @@ -72,7 +72,7 @@ GIFMovie::~GIFMovie() DGifCloseFile(fGIF, nullptr); } -static SkMSec savedimage_duration(const SavedImage* image) +static Movie::MSec savedimage_duration(const SavedImage* image) { for (int j = 0; j < image->ExtensionBlockCount; j++) { @@ -91,7 +91,7 @@ bool GIFMovie::onGetInfo(Info* info) if (nullptr == fGIF) return false; - SkMSec dur = 0; + Movie::MSec dur = 0; for (int i = 0; i < fGIF->ImageCount; i++) dur += savedimage_duration(&fGIF->SavedImages[i]); @@ -102,12 +102,12 @@ bool GIFMovie::onGetInfo(Info* info) return true; } -bool GIFMovie::onSetTime(SkMSec time) +bool GIFMovie::onSetTime(Movie::MSec time) { if (nullptr == fGIF) return false; - SkMSec dur = 0; + Movie::MSec dur = 0; for (int i = 0; i < fGIF->ImageCount; i++) { dur += savedimage_duration(&fGIF->SavedImages[i]); diff --git a/libs/hwui/jni/Gainmap.cpp b/libs/hwui/jni/Gainmap.cpp index 0fffee744be0..71972d01b94f 100644 --- a/libs/hwui/jni/Gainmap.cpp +++ b/libs/hwui/jni/Gainmap.cpp @@ -16,6 +16,9 @@ #include <Gainmap.h> +#include "SkColorType.h" +#include "SkGainmapInfo.h" + #ifdef __ANDROID__ #include <binder/Parcel.h> #endif @@ -36,6 +39,28 @@ static Gainmap* fromJava(jlong gainmap) { return reinterpret_cast<Gainmap*>(gainmap); } +static SkGainmapInfo::BaseImageType baseImageTypeFromJava(jint direction) { + switch (direction) { + case 0: + return SkGainmapInfo::BaseImageType::kSDR; + case 1: + return SkGainmapInfo::BaseImageType::kHDR; + default: + LOG_ALWAYS_FATAL("Unrecognized Gainmap direction: %d", direction); + } +} + +static jint baseImageTypeToJava(SkGainmapInfo::BaseImageType type) { + switch (type) { + case SkGainmapInfo::BaseImageType::kSDR: + return 0; + case SkGainmapInfo::BaseImageType::kHDR: + return 1; + default: + LOG_ALWAYS_FATAL("Unrecognized base image: %d", type); + } +} + static int getCreateFlags(const sk_sp<Bitmap>& bitmap) { int flags = 0; if (bitmap->info().alphaType() == kPremul_SkAlphaType) { @@ -169,6 +194,36 @@ static jfloat Gainmap_getDisplayRatioSdr(JNIEnv*, jobject, jlong gainmapPtr) { return fromJava(gainmapPtr)->info.fDisplayRatioSdr; } +static void Gainmap_setAlternativeColorSpace(JNIEnv*, jobject, jlong gainmapPtr, + jlong colorSpacePtr) { + auto colorSpace = GraphicsJNI::getNativeColorSpace(colorSpacePtr); + fromJava(gainmapPtr)->info.fGainmapMathColorSpace = colorSpace; +} + +static jobject Gainmap_getAlternativeColorSpace(JNIEnv* env, jobject, jlong gainmapPtr) { + const auto javaGainmap = fromJava(gainmapPtr); + auto colorSpace = javaGainmap->info.fGainmapMathColorSpace.get(); + if (colorSpace == nullptr) { + return nullptr; + } + + auto colorType = javaGainmap->bitmap->colorType(); + // A8 bitmaps don't support colorspaces, but an alternative colorspace is + // still valid for configuring the gainmap math, so use RGBA8888 instead. + if (colorType == kAlpha_8_SkColorType) { + colorType = kRGBA_8888_SkColorType; + } + return GraphicsJNI::getColorSpace(env, colorSpace, colorType); +} + +static void Gainmap_setDirection(JNIEnv*, jobject, jlong gainmapPtr, jint direction) { + fromJava(gainmapPtr)->info.fBaseImageType = baseImageTypeFromJava(direction); +} + +static jint Gainmap_getDirection(JNIEnv* env, jobject, jlong gainmapPtr) { + return baseImageTypeToJava(fromJava(gainmapPtr)->info.fBaseImageType); +} + // ---------------------------------------------------------------------------- // Serialization // ---------------------------------------------------------------------------- @@ -260,6 +315,11 @@ static const JNINativeMethod gGainmapMethods[] = { {"nGetDisplayRatioHdr", "(J)F", (void*)Gainmap_getDisplayRatioHdr}, {"nSetDisplayRatioSdr", "(JF)V", (void*)Gainmap_setDisplayRatioSdr}, {"nGetDisplayRatioSdr", "(J)F", (void*)Gainmap_getDisplayRatioSdr}, + {"nSetAlternativeColorSpace", "(JJ)V", (void*)Gainmap_setAlternativeColorSpace}, + {"nGetAlternativeColorSpace", "(J)Landroid/graphics/ColorSpace;", + (void*)Gainmap_getAlternativeColorSpace}, + {"nSetDirection", "(JI)V", (void*)Gainmap_setDirection}, + {"nGetDirection", "(J)I", (void*)Gainmap_getDirection}, {"nWriteGainmapToParcel", "(JLandroid/os/Parcel;)V", (void*)Gainmap_writeToParcel}, {"nReadGainmapFromParcel", "(JLandroid/os/Parcel;)V", (void*)Gainmap_readFromParcel}, }; diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp index a88139d6b5d6..258bf91f2124 100644 --- a/libs/hwui/jni/Graphics.cpp +++ b/libs/hwui/jni/Graphics.cpp @@ -1,12 +1,14 @@ #include <assert.h> +#include <cutils/ashmem.h> +#include <hwui/Canvas.h> +#include <log/log.h> +#include <nativehelper/JNIHelp.h> #include <unistd.h> -#include "jni.h" -#include <nativehelper/JNIHelp.h> #include "GraphicsJNI.h" - #include "SkBitmap.h" #include "SkCanvas.h" +#include "SkColor.h" #include "SkColorSpace.h" #include "SkFontMetrics.h" #include "SkImageInfo.h" @@ -14,10 +16,9 @@ #include "SkPoint.h" #include "SkRect.h" #include "SkRegion.h" +#include "SkSamplingOptions.h" #include "SkTypes.h" -#include <cutils/ashmem.h> -#include <hwui/Canvas.h> -#include <log/log.h> +#include "jni.h" using namespace android; @@ -630,13 +631,15 @@ bool HeapAllocator::allocPixelRef(SkBitmap* bitmap) { //////////////////////////////////////////////////////////////////////////////// -RecyclingClippingPixelAllocator::RecyclingClippingPixelAllocator(android::Bitmap* recycledBitmap, - bool mustMatchColorType) +RecyclingClippingPixelAllocator::RecyclingClippingPixelAllocator( + android::Bitmap* recycledBitmap, bool mustMatchColorType, + std::optional<SkRect> desiredSubset) : mRecycledBitmap(recycledBitmap) , mRecycledBytes(recycledBitmap ? recycledBitmap->getAllocationByteCount() : 0) , mSkiaBitmap(nullptr) , mNeedsCopy(false) - , mMustMatchColorType(mustMatchColorType) {} + , mMustMatchColorType(mustMatchColorType) + , mDesiredSubset(getSourceBoundsForUpsample(desiredSubset)) {} RecyclingClippingPixelAllocator::~RecyclingClippingPixelAllocator() {} @@ -668,7 +671,8 @@ bool RecyclingClippingPixelAllocator::allocPixelRef(SkBitmap* bitmap) { const SkImageInfo maxInfo = bitmap->info().makeWH(maxWidth, maxHeight); const size_t rowBytes = maxInfo.minRowBytes(); const size_t bytesNeeded = maxInfo.computeByteSize(rowBytes); - if (bytesNeeded <= mRecycledBytes) { + + if (!mDesiredSubset && bytesNeeded <= mRecycledBytes) { // Here we take advantage of reconfigure() to reset the rowBytes // of mRecycledBitmap. It is very important that we pass in // mRecycledBitmap->info() for the SkImageInfo. According to the @@ -712,20 +716,31 @@ void RecyclingClippingPixelAllocator::copyIfNecessary() { if (mNeedsCopy) { mRecycledBitmap->ref(); android::Bitmap* recycledPixels = mRecycledBitmap; - void* dst = recycledPixels->pixels(); - const size_t dstRowBytes = mRecycledBitmap->rowBytes(); - const size_t bytesToCopy = std::min(mRecycledBitmap->info().minRowBytes(), - mSkiaBitmap->info().minRowBytes()); - const int rowsToCopy = std::min(mRecycledBitmap->info().height(), - mSkiaBitmap->info().height()); - for (int y = 0; y < rowsToCopy; y++) { - memcpy(dst, mSkiaBitmap->getAddr(0, y), bytesToCopy); - // Cast to bytes in order to apply the dstRowBytes offset correctly. - dst = reinterpret_cast<void*>( - reinterpret_cast<uint8_t*>(dst) + dstRowBytes); + if (mDesiredSubset) { + recycledPixels->setAlphaType(mSkiaBitmap->alphaType()); + recycledPixels->setColorSpace(mSkiaBitmap->refColorSpace()); + + auto canvas = SkCanvas(recycledPixels->getSkBitmap()); + SkRect destination = SkRect::Make(recycledPixels->info().bounds()); + destination.intersect(SkRect::Make(mSkiaBitmap->info().bounds())); + canvas.drawImageRect(mSkiaBitmap->asImage(), *mDesiredSubset, destination, + SkSamplingOptions(SkFilterMode::kLinear), nullptr, + SkCanvas::kFast_SrcRectConstraint); + } else { + void* dst = recycledPixels->pixels(); + const size_t dstRowBytes = mRecycledBitmap->rowBytes(); + const size_t bytesToCopy = std::min(mRecycledBitmap->info().minRowBytes(), + mSkiaBitmap->info().minRowBytes()); + const int rowsToCopy = + std::min(mRecycledBitmap->info().height(), mSkiaBitmap->info().height()); + for (int y = 0; y < rowsToCopy; y++) { + memcpy(dst, mSkiaBitmap->getAddr(0, y), bytesToCopy); + // Cast to bytes in order to apply the dstRowBytes offset correctly. + dst = reinterpret_cast<void*>(reinterpret_cast<uint8_t*>(dst) + dstRowBytes); + } + recycledPixels->setAlphaType(mSkiaBitmap->alphaType()); + recycledPixels->setColorSpace(mSkiaBitmap->refColorSpace()); } - recycledPixels->setAlphaType(mSkiaBitmap->alphaType()); - recycledPixels->setColorSpace(mSkiaBitmap->refColorSpace()); recycledPixels->notifyPixelsChanged(); recycledPixels->unref(); } @@ -733,6 +748,20 @@ void RecyclingClippingPixelAllocator::copyIfNecessary() { mSkiaBitmap = nullptr; } +std::optional<SkRect> RecyclingClippingPixelAllocator::getSourceBoundsForUpsample( + std::optional<SkRect> subset) { + if (!uirenderer::Properties::resampleGainmapRegions || !subset || subset->isEmpty()) { + return std::nullopt; + } + + if (subset->left() == floor(subset->left()) && subset->top() == floor(subset->top()) && + subset->right() == floor(subset->right()) && subset->bottom() == floor(subset->bottom())) { + return std::nullopt; + } + + return subset; +} + //////////////////////////////////////////////////////////////////////////////// AshmemPixelAllocator::AshmemPixelAllocator(JNIEnv *env) { diff --git a/libs/hwui/jni/GraphicsJNI.h b/libs/hwui/jni/GraphicsJNI.h index b0a1074d6693..4b08f8dc7a93 100644 --- a/libs/hwui/jni/GraphicsJNI.h +++ b/libs/hwui/jni/GraphicsJNI.h @@ -216,8 +216,8 @@ private: */ class RecyclingClippingPixelAllocator : public android::skia::BRDAllocator { public: - RecyclingClippingPixelAllocator(android::Bitmap* recycledBitmap, - bool mustMatchColorType = true); + RecyclingClippingPixelAllocator(android::Bitmap* recycledBitmap, bool mustMatchColorType = true, + std::optional<SkRect> desiredSubset = std::nullopt); ~RecyclingClippingPixelAllocator(); @@ -241,11 +241,24 @@ public: SkCodec::ZeroInitialized zeroInit() const override { return SkCodec::kNo_ZeroInitialized; } private: + /** + * Optionally returns a subset rectangle that we need to upsample from. + * E.g., a gainmap subset may be decoded in a slightly larger rectangle + * than is needed (in order to correctly preserve gainmap alignment when + * rendering at display time), so we need to re-sample the "intended" + * gainmap back up to the bitmap dimensions. + * + * If we don't need to upsample from a subregion, then returns an empty + * optional + */ + static std::optional<SkRect> getSourceBoundsForUpsample(std::optional<SkRect> subset); + android::Bitmap* mRecycledBitmap; const size_t mRecycledBytes; SkBitmap* mSkiaBitmap; bool mNeedsCopy; const bool mMustMatchColorType; + const std::optional<SkRect> mDesiredSubset; }; class AshmemPixelAllocator : public SkBitmap::Allocator { diff --git a/libs/hwui/jni/GraphicsStatsService.cpp b/libs/hwui/jni/GraphicsStatsService.cpp index 54369b9e4384..80a8ae1d8c17 100644 --- a/libs/hwui/jni/GraphicsStatsService.cpp +++ b/libs/hwui/jni/GraphicsStatsService.cpp @@ -42,8 +42,9 @@ static jlong createDump(JNIEnv*, jobject, jint fd, jboolean isProto) { return reinterpret_cast<jlong>(dump); } -static void addToDump(JNIEnv* env, jobject, jlong dumpPtr, jstring jpath, jstring jpackage, - jlong versionCode, jlong startTime, jlong endTime, jbyteArray jdata) { +static void addToDump(JNIEnv* env, jobject, jlong dumpPtr, jstring jpath, jint uid, + jstring jpackage, jlong versionCode, jlong startTime, jlong endTime, + jbyteArray jdata) { std::string path; const ProfileData* data = nullptr; LOG_ALWAYS_FATAL_IF(jdata == nullptr && jpath == nullptr, "Path and data can't both be null"); @@ -68,7 +69,8 @@ static void addToDump(JNIEnv* env, jobject, jlong dumpPtr, jstring jpath, jstrin LOG_ALWAYS_FATAL_IF(!dump, "null passed for dump pointer"); const std::string package(packageChars.c_str(), packageChars.size()); - GraphicsStatsService::addToDump(dump, path, package, versionCode, startTime, endTime, data); + GraphicsStatsService::addToDump(dump, path, static_cast<uid_t>(uid), package, versionCode, + startTime, endTime, data); } static void addFileToDump(JNIEnv* env, jobject, jlong dumpPtr, jstring jpath) { @@ -91,7 +93,7 @@ static void finishDumpInMemory(JNIEnv* env, jobject, jlong dumpPtr, jlong pulled GraphicsStatsService::finishDumpInMemory(dump, data, lastFullDay == JNI_TRUE); } -static void saveBuffer(JNIEnv* env, jobject clazz, jstring jpath, jstring jpackage, +static void saveBuffer(JNIEnv* env, jobject clazz, jstring jpath, jint uid, jstring jpackage, jlong versionCode, jlong startTime, jlong endTime, jbyteArray jdata) { ScopedByteArrayRO buffer(env, jdata); LOG_ALWAYS_FATAL_IF(buffer.size() != sizeof(ProfileData), @@ -106,7 +108,8 @@ static void saveBuffer(JNIEnv* env, jobject clazz, jstring jpath, jstring jpacka const std::string path(pathChars.c_str(), pathChars.size()); const std::string package(packageChars.c_str(), packageChars.size()); const ProfileData* data = reinterpret_cast<const ProfileData*>(buffer.get()); - GraphicsStatsService::saveBuffer(path, package, versionCode, startTime, endTime, data); + GraphicsStatsService::saveBuffer(path, static_cast<uid_t>(uid), package, versionCode, startTime, + endTime, data); } static jobject gGraphicsStatsServiceObject = nullptr; @@ -173,16 +176,16 @@ static void nativeDestructor(JNIEnv* env, jobject javaObject) { } // namespace android using namespace android; -static const JNINativeMethod sMethods[] = - {{"nGetAshmemSize", "()I", (void*)getAshmemSize}, - {"nCreateDump", "(IZ)J", (void*)createDump}, - {"nAddToDump", "(JLjava/lang/String;Ljava/lang/String;JJJ[B)V", (void*)addToDump}, - {"nAddToDump", "(JLjava/lang/String;)V", (void*)addFileToDump}, - {"nFinishDump", "(J)V", (void*)finishDump}, - {"nFinishDumpInMemory", "(JJZ)V", (void*)finishDumpInMemory}, - {"nSaveBuffer", "(Ljava/lang/String;Ljava/lang/String;JJJ[B)V", (void*)saveBuffer}, - {"nativeInit", "()V", (void*)nativeInit}, - {"nativeDestructor", "()V", (void*)nativeDestructor}}; +static const JNINativeMethod sMethods[] = { + {"nGetAshmemSize", "()I", (void*)getAshmemSize}, + {"nCreateDump", "(IZ)J", (void*)createDump}, + {"nAddToDump", "(JLjava/lang/String;ILjava/lang/String;JJJ[B)V", (void*)addToDump}, + {"nAddToDump", "(JLjava/lang/String;)V", (void*)addFileToDump}, + {"nFinishDump", "(J)V", (void*)finishDump}, + {"nFinishDumpInMemory", "(JJZ)V", (void*)finishDumpInMemory}, + {"nSaveBuffer", "(Ljava/lang/String;ILjava/lang/String;JJJ[B)V", (void*)saveBuffer}, + {"nativeInit", "()V", (void*)nativeInit}, + {"nativeDestructor", "()V", (void*)nativeDestructor}}; int register_android_graphics_GraphicsStatsService(JNIEnv* env) { jclass graphicsStatsService_class = diff --git a/libs/hwui/jni/ImageDecoder.cpp b/libs/hwui/jni/ImageDecoder.cpp index 6744c6c0b9ec..aebc4db37898 100644 --- a/libs/hwui/jni/ImageDecoder.cpp +++ b/libs/hwui/jni/ImageDecoder.cpp @@ -162,8 +162,8 @@ static jobject native_create(JNIEnv* env, std::unique_ptr<SkStream> stream, static jobject ImageDecoder_nCreateFd(JNIEnv* env, jobject /*clazz*/, jobject fileDescriptor, jlong length, jboolean preferAnimation, jobject source) { -#ifndef __ANDROID__ // LayoutLib for Windows does not support F_DUPFD_CLOEXEC - return throw_exception(env, kSourceException, "Only supported on Android", nullptr, source); +#ifdef _WIN32 // LayoutLib for Windows does not support F_DUPFD_CLOEXEC + return throw_exception(env, kSourceException, "Not supported on Windows", nullptr, source); #else int descriptor = jniGetFDFromFileDescriptor(env, fileDescriptor); diff --git a/libs/hwui/jni/MeshSpecification.cpp b/libs/hwui/jni/MeshSpecification.cpp index ae9792df3d82..b943496ae9f7 100644 --- a/libs/hwui/jni/MeshSpecification.cpp +++ b/libs/hwui/jni/MeshSpecification.cpp @@ -126,7 +126,7 @@ static void MeshSpecification_safeUnref(SkMeshSpecification* meshSpec) { SkSafeUnref(meshSpec); } -static jlong getMeshSpecificationFinalizer() { +static jlong getMeshSpecificationFinalizer(CRITICAL_JNI_PARAMS) { return static_cast<jlong>(reinterpret_cast<uintptr_t>(&MeshSpecification_safeUnref)); } diff --git a/libs/hwui/jni/Movie.h b/libs/hwui/jni/Movie.h index 02113dd58ec8..d633d935f566 100644 --- a/libs/hwui/jni/Movie.h +++ b/libs/hwui/jni/Movie.h @@ -19,6 +19,8 @@ class SkStreamRewindable; class Movie : public SkRefCnt { public: + using MSec = uint32_t; // millisecond duration + /** Try to create a movie from the stream. If the stream format is not supported, return NULL. */ @@ -36,7 +38,7 @@ public: */ static Movie* DecodeMemory(const void* data, size_t length); - SkMSec duration(); + MSec duration(); int width(); int height(); int isOpaque(); @@ -46,21 +48,21 @@ public: bitmap/frame from the previous state (i.e. true means you need to redraw). */ - bool setTime(SkMSec); + bool setTime(MSec); // return the right bitmap for the current time code const SkBitmap& bitmap(); protected: struct Info { - SkMSec fDuration; + MSec fDuration; int fWidth; int fHeight; bool fIsOpaque; }; virtual bool onGetInfo(Info*) = 0; - virtual bool onSetTime(SkMSec) = 0; + virtual bool onSetTime(MSec) = 0; virtual bool onGetBitmap(SkBitmap*) = 0; // visible for subclasses @@ -68,7 +70,7 @@ protected: private: Info fInfo; - SkMSec fCurrTime; + MSec fCurrTime; SkBitmap fBitmap; bool fNeedBitmap; diff --git a/libs/hwui/jni/MovieImpl.cpp b/libs/hwui/jni/MovieImpl.cpp index abb75fa99c94..a31a15f061b1 100644 --- a/libs/hwui/jni/MovieImpl.cpp +++ b/libs/hwui/jni/MovieImpl.cpp @@ -11,7 +11,7 @@ // We should never see this in normal operation since our time values are // 0-based. So we use it as a sentinel. -#define UNINITIALIZED_MSEC ((SkMSec)-1) +#define UNINITIALIZED_MSEC ((Movie::MSec)-1) Movie::Movie() { @@ -26,7 +26,7 @@ void Movie::ensureInfo() memset(&fInfo, 0, sizeof(fInfo)); // failure } -SkMSec Movie::duration() +Movie::MSec Movie::duration() { this->ensureInfo(); return fInfo.fDuration; @@ -50,9 +50,9 @@ int Movie::isOpaque() return fInfo.fIsOpaque; } -bool Movie::setTime(SkMSec time) +bool Movie::setTime(Movie::MSec time) { - SkMSec dur = this->duration(); + Movie::MSec dur = this->duration(); if (time > dur) time = dur; diff --git a/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp index e3cdee6e7034..3b1b86160f3a 100644 --- a/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp +++ b/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp @@ -135,7 +135,7 @@ static void android_graphics_HardwareBufferRenderer_setLightAlpha(JNIEnv* env, j proxy->setLightAlpha((uint8_t)(255 * ambientShadowAlpha), (uint8_t)(255 * spotShadowAlpha)); } -static jlong android_graphics_HardwareBufferRenderer_getFinalizer() { +static jlong android_graphics_HardwareBufferRenderer_getFinalizer(CRITICAL_JNI_PARAMS) { return static_cast<jlong>(reinterpret_cast<uintptr_t>(&HardwareBufferRenderer_destroy)); } diff --git a/libs/hwui/libhwui.map.txt b/libs/hwui/libhwui.map.txt index d03ceb471d6c..2414299321a9 100644 --- a/libs/hwui/libhwui.map.txt +++ b/libs/hwui/libhwui.map.txt @@ -13,6 +13,7 @@ LIBHWUI { # platform-only /* HWUI isn't current a module, so all of these are st ABitmapConfig_getFormatFromConfig; ABitmapConfig_getConfigFromFormat; ABitmap_compress; + ABitmap_compressWithGainmap; ABitmap_getHardwareBuffer; ACanvas_isSupportedPixelFormat; ACanvas_getNativeHandleFromJava; diff --git a/libs/hwui/pipeline/skia/ATraceMemoryDump.cpp b/libs/hwui/pipeline/skia/ATraceMemoryDump.cpp index 756b937f7de3..38006454b4c7 100644 --- a/libs/hwui/pipeline/skia/ATraceMemoryDump.cpp +++ b/libs/hwui/pipeline/skia/ATraceMemoryDump.cpp @@ -16,12 +16,11 @@ #include "ATraceMemoryDump.h" +#include <include/gpu/ganesh/GrDirectContext.h> #include <utils/Trace.h> #include <cstring> -#include "GrDirectContext.h" - namespace android { namespace uirenderer { namespace skiapipeline { diff --git a/libs/hwui/pipeline/skia/ATraceMemoryDump.h b/libs/hwui/pipeline/skia/ATraceMemoryDump.h index 777d1a2ddb5b..86ff33ff22a2 100644 --- a/libs/hwui/pipeline/skia/ATraceMemoryDump.h +++ b/libs/hwui/pipeline/skia/ATraceMemoryDump.h @@ -16,9 +16,9 @@ #pragma once -#include <GrDirectContext.h> #include <SkString.h> #include <SkTraceMemoryDump.h> +#include <include/gpu/ganesh/GrDirectContext.h> #include <string> #include <unordered_map> diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp index 5d3fb30769ed..85432bfb557a 100644 --- a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp @@ -15,24 +15,26 @@ */ #include "GLFunctorDrawable.h" -#include <GrDirectContext.h> + +#include <effects/GainmapRenderer.h> +#include <include/gpu/GpuTypes.h> +#include <include/gpu/ganesh/GrBackendSurface.h> +#include <include/gpu/ganesh/GrDirectContext.h> +#include <include/gpu/ganesh/SkSurfaceGanesh.h> +#include <include/gpu/ganesh/gl/GrGLBackendSurface.h> +#include <include/gpu/ganesh/gl/GrGLTypes.h> #include <private/hwui/DrawGlInfo.h> + #include "FunctorDrawable.h" -#include "GrBackendSurface.h" #include "RenderNode.h" #include "SkAndroidFrameworkUtils.h" #include "SkCanvas.h" #include "SkCanvasAndroid.h" #include "SkClipStack.h" -#include "SkRect.h" #include "SkM44.h" -#include <include/gpu/ganesh/SkSurfaceGanesh.h> -#include "include/gpu/GpuTypes.h" // from Skia -#include <include/gpu/gl/GrGLTypes.h> -#include <include/gpu/ganesh/gl/GrGLBackendSurface.h> -#include "utils/GLUtils.h" -#include <effects/GainmapRenderer.h> +#include "SkRect.h" #include "renderthread/CanvasContext.h" +#include "utils/GLUtils.h" namespace android { namespace uirenderer { diff --git a/libs/hwui/pipeline/skia/LayerDrawable.cpp b/libs/hwui/pipeline/skia/LayerDrawable.cpp index 99f54c19d2e5..8f3366c2f912 100644 --- a/libs/hwui/pipeline/skia/LayerDrawable.cpp +++ b/libs/hwui/pipeline/skia/LayerDrawable.cpp @@ -16,17 +16,17 @@ #include "LayerDrawable.h" +#include <include/gpu/ganesh/GrBackendSurface.h> +#include <include/gpu/ganesh/gl/GrGLTypes.h> #include <shaders/shaders.h> #include <utils/Color.h> #include <utils/MathUtils.h> #include "DeviceInfo.h" -#include "GrBackendSurface.h" #include "SkColorFilter.h" #include "SkRuntimeEffect.h" #include "SkSurface.h" #include "Tonemapper.h" -#include "gl/GrGLTypes.h" #include "math/mat4.h" #include "system/graphics-base-v1.0.h" #include "system/window.h" diff --git a/libs/hwui/pipeline/skia/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp index 8e07a2f31de1..22f59a67bccb 100644 --- a/libs/hwui/pipeline/skia/ShaderCache.cpp +++ b/libs/hwui/pipeline/skia/ShaderCache.cpp @@ -16,9 +16,9 @@ #include "ShaderCache.h" -#include <GrDirectContext.h> #include <SkData.h> #include <gui/TraceUtils.h> +#include <include/gpu/ganesh/GrDirectContext.h> #include <log/log.h> #include <openssl/sha.h> diff --git a/libs/hwui/pipeline/skia/ShaderCache.h b/libs/hwui/pipeline/skia/ShaderCache.h index 40dfc9d4309b..4c011613710b 100644 --- a/libs/hwui/pipeline/skia/ShaderCache.h +++ b/libs/hwui/pipeline/skia/ShaderCache.h @@ -17,10 +17,10 @@ #pragma once #include <FileBlobCache.h> -#include <GrContextOptions.h> #include <SkRefCnt.h> #include <cutils/compiler.h> #include <ftl/shared_mutex.h> +#include <include/gpu/ganesh/GrContextOptions.h> #include <utils/Mutex.h> #include <memory> diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp index e4b1f916b4d6..0768f457972b 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp @@ -16,14 +16,14 @@ #include "pipeline/skia/SkiaOpenGLPipeline.h" -#include <GrBackendSurface.h> #include <SkBlendMode.h> #include <SkImageInfo.h> #include <cutils/properties.h> #include <gui/TraceUtils.h> +#include <include/gpu/ganesh/GrBackendSurface.h> #include <include/gpu/ganesh/SkSurfaceGanesh.h> #include <include/gpu/ganesh/gl/GrGLBackendSurface.h> -#include <include/gpu/gl/GrGLTypes.h> +#include <include/gpu/ganesh/gl/GrGLTypes.h> #include <strings.h> #include "DeferredLayerUpdater.h" diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp index d06dba05ee88..e1de1e632c68 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp @@ -16,14 +16,14 @@ #include "pipeline/skia/SkiaVulkanPipeline.h" -#include <GrDirectContext.h> -#include <GrTypes.h> #include <SkSurface.h> #include <SkTypes.h> #include <cutils/properties.h> #include <gui/TraceUtils.h> +#include <include/gpu/ganesh/GrDirectContext.h> +#include <include/gpu/ganesh/GrTypes.h> +#include <include/gpu/ganesh/vk/GrVkTypes.h> #include <strings.h> -#include <vk/GrVkTypes.h> #include "DeferredLayerUpdater.h" #include "LightingInfo.h" diff --git a/libs/hwui/pipeline/skia/StretchMask.h b/libs/hwui/pipeline/skia/StretchMask.h index dc698b8e57ff..0baed9fcb2b6 100644 --- a/libs/hwui/pipeline/skia/StretchMask.h +++ b/libs/hwui/pipeline/skia/StretchMask.h @@ -15,9 +15,10 @@ */ #pragma once -#include "GrRecordingContext.h" -#include <effects/StretchEffect.h> #include <SkSurface.h> +#include <effects/StretchEffect.h> +#include <include/gpu/ganesh/GrRecordingContext.h> + #include "SkiaDisplayList.h" namespace android::uirenderer { diff --git a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp index 21fe6ff14f56..1ebc3c86b3a7 100644 --- a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp @@ -19,12 +19,12 @@ #include <SkAndroidFrameworkUtils.h> #include <SkImage.h> #include <SkM44.h> -#include <include/gpu/ganesh/vk/GrBackendDrawableInfo.h> #include <gui/TraceUtils.h> +#include <include/gpu/ganesh/vk/GrBackendDrawableInfo.h> +#include <include/gpu/ganesh/vk/GrVkTypes.h> #include <private/hwui/DrawVkInfo.h> #include <utils/Color.h> #include <utils/Trace.h> -#include <vk/GrVkTypes.h> #include <thread> diff --git a/libs/hwui/platform/host/thread/ThreadBase.h b/libs/hwui/platform/host/thread/ThreadBase.h index d709430cc9b6..b4e7bd34971a 100644 --- a/libs/hwui/platform/host/thread/ThreadBase.h +++ b/libs/hwui/platform/host/thread/ThreadBase.h @@ -48,7 +48,7 @@ protected: nsecs_t nextWakeup = mQueue.nextWakeup(lock); std::chrono::nanoseconds duration = std::chrono::nanoseconds::max(); if (nextWakeup < std::numeric_limits<nsecs_t>::max()) { - int timeout = nextWakeup - WorkQueue::clock::now(); + nsecs_t timeout = nextWakeup - WorkQueue::clock::now(); if (timeout < 0) timeout = 0; duration = std::chrono::nanoseconds(timeout); } diff --git a/libs/hwui/protos/graphicsstats.proto b/libs/hwui/protos/graphicsstats.proto index 745393ce1a3d..a6e786c07053 100644 --- a/libs/hwui/protos/graphicsstats.proto +++ b/libs/hwui/protos/graphicsstats.proto @@ -58,6 +58,9 @@ message GraphicsStatsProto { // HWUI renders pipeline type: GL or Vulkan optional PipelineType pipeline = 8; + + // The UID of the app + optional int32 uid = 9; } message GraphicsStatsJankSummaryProto { diff --git a/libs/hwui/renderthread/CacheManager.cpp b/libs/hwui/renderthread/CacheManager.cpp index ac2a9366a1f6..277178027383 100644 --- a/libs/hwui/renderthread/CacheManager.cpp +++ b/libs/hwui/renderthread/CacheManager.cpp @@ -16,10 +16,10 @@ #include "CacheManager.h" -#include <GrContextOptions.h> -#include <GrTypes.h> #include <SkExecutor.h> #include <SkGraphics.h> +#include <include/gpu/ganesh/GrContextOptions.h> +#include <include/gpu/ganesh/GrTypes.h> #include <math.h> #include <utils/Trace.h> diff --git a/libs/hwui/renderthread/CacheManager.h b/libs/hwui/renderthread/CacheManager.h index bcfa4f359d83..94f10051688e 100644 --- a/libs/hwui/renderthread/CacheManager.h +++ b/libs/hwui/renderthread/CacheManager.h @@ -18,7 +18,7 @@ #define CACHEMANAGER_H #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration -#include <GrDirectContext.h> +#include <include/gpu/ganesh/GrDirectContext.h> #endif #include <SkSurface.h> #include <utils/String8.h> diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index 8bb11badb607..8ec04304a808 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -654,6 +654,9 @@ void CanvasContext::draw(bool solelyTextureViewUpdates) { if (vsyncId != UiFrameInfoBuilder::INVALID_VSYNC_ID) { const auto inputEventId = static_cast<int32_t>(mCurrentFrameInfo->get(FrameInfoIndex::InputEventId)); + ATRACE_FORMAT( + "frameTimelineInfo(frameNumber=%llu, vsyncId=%lld, inputEventId=0x%" PRIx32 ")", + frameCompleteNr, vsyncId, inputEventId); const ANativeWindowFrameTimelineInfo ftl = { .frameNumber = frameCompleteNr, .frameTimelineVsyncId = vsyncId, @@ -761,8 +764,8 @@ void CanvasContext::draw(bool solelyTextureViewUpdates) { if (mExpectSurfaceStats) { reportMetricsWithPresentTime(); { // acquire lock - std::lock_guard lock(mLast4FrameMetricsInfosMutex); - FrameMetricsInfo& next = mLast4FrameMetricsInfos.next(); + std::lock_guard lock(mLastFrameMetricsInfosMutex); + FrameMetricsInfo& next = mLastFrameMetricsInfos.next(); next.frameInfo = mCurrentFrameInfo; next.frameNumber = frameCompleteNr; next.surfaceId = mSurfaceControlGenerationId; @@ -816,12 +819,12 @@ void CanvasContext::reportMetricsWithPresentTime() { int32_t surfaceControlId; { // acquire lock - std::scoped_lock lock(mLast4FrameMetricsInfosMutex); - if (mLast4FrameMetricsInfos.size() != mLast4FrameMetricsInfos.capacity()) { + std::scoped_lock lock(mLastFrameMetricsInfosMutex); + if (mLastFrameMetricsInfos.size() != mLastFrameMetricsInfos.capacity()) { // Not enough frames yet return; } - auto frameMetricsInfo = mLast4FrameMetricsInfos.front(); + auto frameMetricsInfo = mLastFrameMetricsInfos.front(); forthBehind = frameMetricsInfo.frameInfo; frameNumber = frameMetricsInfo.frameNumber; surfaceControlId = frameMetricsInfo.surfaceId; @@ -869,12 +872,12 @@ void CanvasContext::removeFrameMetricsObserver(FrameMetricsObserver* observer) { } } -FrameInfo* CanvasContext::getFrameInfoFromLast4(uint64_t frameNumber, uint32_t surfaceControlId) { - std::scoped_lock lock(mLast4FrameMetricsInfosMutex); - for (size_t i = 0; i < mLast4FrameMetricsInfos.size(); i++) { - if (mLast4FrameMetricsInfos[i].frameNumber == frameNumber && - mLast4FrameMetricsInfos[i].surfaceId == surfaceControlId) { - return mLast4FrameMetricsInfos[i].frameInfo; +FrameInfo* CanvasContext::getFrameInfoFromLastFew(uint64_t frameNumber, uint32_t surfaceControlId) { + std::scoped_lock lock(mLastFrameMetricsInfosMutex); + for (size_t i = 0; i < mLastFrameMetricsInfos.size(); i++) { + if (mLastFrameMetricsInfos[i].frameNumber == frameNumber && + mLastFrameMetricsInfos[i].surfaceId == surfaceControlId) { + return mLastFrameMetricsInfos[i].frameInfo; } } @@ -894,7 +897,7 @@ void CanvasContext::onSurfaceStatsAvailable(void* context, int32_t surfaceContro } uint64_t frameNumber = functions.getFrameNumberFunc(stats); - FrameInfo* frameInfo = instance->getFrameInfoFromLast4(frameNumber, surfaceControlId); + FrameInfo* frameInfo = instance->getFrameInfoFromLastFew(frameNumber, surfaceControlId); if (frameInfo != nullptr) { std::scoped_lock lock(instance->mFrameInfoMutex); diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h index e2e3fa35b9b0..cb3753822035 100644 --- a/libs/hwui/renderthread/CanvasContext.h +++ b/libs/hwui/renderthread/CanvasContext.h @@ -260,7 +260,7 @@ private: void finishFrame(FrameInfo* frameInfo); /** - * Invoke 'reportFrameMetrics' on the last frame stored in 'mLast4FrameInfos'. + * Invoke 'reportFrameMetrics' on the last frame stored in 'mLastFrameInfos'. * Populate the 'presentTime' field before calling. */ void reportMetricsWithPresentTime(); @@ -271,7 +271,7 @@ private: int32_t surfaceId; }; - FrameInfo* getFrameInfoFromLast4(uint64_t frameNumber, uint32_t surfaceControlId); + FrameInfo* getFrameInfoFromLastFew(uint64_t frameNumber, uint32_t surfaceControlId); Frame getFrame(); @@ -336,9 +336,9 @@ private: // List of data of frames that are awaiting GPU completion reporting. Used to compute frame // metrics and determine whether or not to report the metrics. - RingBuffer<FrameMetricsInfo, 4> mLast4FrameMetricsInfos - GUARDED_BY(mLast4FrameMetricsInfosMutex); - std::mutex mLast4FrameMetricsInfosMutex; + RingBuffer<FrameMetricsInfo, 6> mLastFrameMetricsInfos + GUARDED_BY(mLastFrameMetricsInfosMutex); + std::mutex mLastFrameMetricsInfosMutex; std::string mName; JankTracker mJankTracker; diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp index 708b0113e13e..60104452f4c0 100644 --- a/libs/hwui/renderthread/EglManager.cpp +++ b/libs/hwui/renderthread/EglManager.cpp @@ -19,6 +19,7 @@ #include <EGL/eglext.h> #include <GLES/gl.h> #include <cutils/properties.h> +#include <graphicsenv/GpuStatsInfo.h> #include <log/log.h> #include <sync/sync.h> #include <utils/Trace.h> @@ -366,6 +367,10 @@ void EglManager::createContext() { contextAttributes.push_back(EGL_CONTEXT_PRIORITY_LEVEL_IMG); contextAttributes.push_back(Properties::contextPriority); } + if (Properties::skipTelemetry) { + contextAttributes.push_back(EGL_TELEMETRY_HINT_ANDROID); + contextAttributes.push_back(android::GpuStatsInfo::SKIP_TELEMETRY); + } contextAttributes.push_back(EGL_NONE); mEglContext = eglCreateContext( mEglDisplay, EglExtensions.noConfigContext ? ((EGLConfig) nullptr) : mEglConfig, diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp index a024aeb285f9..92c6ad10d1c7 100644 --- a/libs/hwui/renderthread/RenderThread.cpp +++ b/libs/hwui/renderthread/RenderThread.cpp @@ -16,12 +16,12 @@ #include "RenderThread.h" -#include <GrContextOptions.h> #include <android-base/properties.h> #include <dlfcn.h> -#include <gl/GrGLInterface.h> #include <gui/TraceUtils.h> +#include <include/gpu/ganesh/GrContextOptions.h> #include <include/gpu/ganesh/gl/GrGLDirectContext.h> +#include <include/gpu/ganesh/gl/GrGLInterface.h> #include <private/android/choreographer.h> #include <sys/resource.h> #include <ui/FatVector.h> diff --git a/libs/hwui/renderthread/RenderThread.h b/libs/hwui/renderthread/RenderThread.h index 045d26f1d329..86fddbae0831 100644 --- a/libs/hwui/renderthread/RenderThread.h +++ b/libs/hwui/renderthread/RenderThread.h @@ -17,9 +17,9 @@ #ifndef RENDERTHREAD_H_ #define RENDERTHREAD_H_ -#include <GrDirectContext.h> #include <SkBitmap.h> #include <cutils/compiler.h> +#include <include/gpu/ganesh/GrDirectContext.h> #include <surface_control_private.h> #include <utils/Thread.h> diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index 0d0af1110ca4..e3023937964e 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -18,19 +18,19 @@ #include <EGL/egl.h> #include <EGL/eglext.h> -#include <GrBackendSemaphore.h> -#include <GrBackendSurface.h> -#include <GrDirectContext.h> -#include <GrTypes.h> #include <android/sync.h> #include <gui/TraceUtils.h> +#include <include/gpu/ganesh/GrBackendSemaphore.h> +#include <include/gpu/ganesh/GrBackendSurface.h> +#include <include/gpu/ganesh/GrDirectContext.h> +#include <include/gpu/ganesh/GrTypes.h> #include <include/gpu/ganesh/SkSurfaceGanesh.h> #include <include/gpu/ganesh/vk/GrVkBackendSemaphore.h> #include <include/gpu/ganesh/vk/GrVkBackendSurface.h> #include <include/gpu/ganesh/vk/GrVkDirectContext.h> +#include <include/gpu/ganesh/vk/GrVkTypes.h> +#include <include/gpu/vk/VulkanBackendContext.h> #include <ui/FatVector.h> -#include <vk/GrVkExtensions.h> -#include <vk/GrVkTypes.h> #include <sstream> @@ -141,7 +141,8 @@ VulkanManager::~VulkanManager() { mPhysicalDeviceFeatures2 = {}; } -void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFeatures2& features) { +void VulkanManager::setupDevice(skgpu::VulkanExtensions& grExtensions, + VkPhysicalDeviceFeatures2& features) { VkResult err; constexpr VkApplicationInfo app_info = { @@ -317,6 +318,15 @@ void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFe tailPNext = &deviceFaultFeatures->pNext; } + if (grExtensions.hasExtension(VK_EXT_RGBA10X6_FORMATS_EXTENSION_NAME, 1)) { + VkPhysicalDeviceRGBA10X6FormatsFeaturesEXT* formatFeatures = + new VkPhysicalDeviceRGBA10X6FormatsFeaturesEXT; + formatFeatures->sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RGBA10X6_FORMATS_FEATURES_EXT; + formatFeatures->pNext = nullptr; + *tailPNext = formatFeatures; + tailPNext = &formatFeatures->pNext; + } + // query to get the physical device features mGetPhysicalDeviceFeatures2(mPhysicalDevice, &features); // this looks like it would slow things down, @@ -506,7 +516,7 @@ sk_sp<GrDirectContext> VulkanManager::createContext(GrContextOptions& options, return vkGetInstanceProcAddr(instance, proc_name); }; - GrVkBackendContext backendContext; + skgpu::VulkanBackendContext backendContext; backendContext.fInstance = mInstance; backendContext.fPhysicalDevice = mPhysicalDevice; backendContext.fDevice = mDevice; diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h index b92ebb3cdf71..f0425719ea89 100644 --- a/libs/hwui/renderthread/VulkanManager.h +++ b/libs/hwui/renderthread/VulkanManager.h @@ -20,12 +20,11 @@ #if !defined(VK_USE_PLATFORM_ANDROID_KHR) #define VK_USE_PLATFORM_ANDROID_KHR #endif -#include <GrContextOptions.h> #include <SkSurface.h> #include <android-base/unique_fd.h> +#include <include/gpu/ganesh/GrContextOptions.h> #include <utils/StrongPointer.h> -#include <vk/GrVkBackendContext.h> -#include <vk/GrVkExtensions.h> +#include <vk/VulkanExtensions.h> #include <vulkan/vulkan.h> // VK_ANDROID_frame_boundary is a bespoke extension defined by AGI @@ -127,7 +126,7 @@ private: // Sets up the VkInstance and VkDevice objects. Also fills out the passed in // VkPhysicalDeviceFeatures struct. - void setupDevice(GrVkExtensions&, VkPhysicalDeviceFeatures2&); + void setupDevice(skgpu::VulkanExtensions&, VkPhysicalDeviceFeatures2&); // simple wrapper class that exists only to initialize a pointer to NULL template <typename FNPTR_TYPE> @@ -206,7 +205,7 @@ private: BufferAge, }; SwapBehavior mSwapBehavior = SwapBehavior::Discard; - GrVkExtensions mExtensions; + skgpu::VulkanExtensions mExtensions; uint32_t mDriverVersion = 0; std::once_flag mInitFlag; diff --git a/libs/hwui/renderthread/VulkanSurface.cpp b/libs/hwui/renderthread/VulkanSurface.cpp index 0f29613cad33..20c2b1acaf02 100644 --- a/libs/hwui/renderthread/VulkanSurface.cpp +++ b/libs/hwui/renderthread/VulkanSurface.cpp @@ -16,12 +16,13 @@ #include "VulkanSurface.h" -#include <include/android/SkSurfaceAndroid.h> -#include <GrDirectContext.h> #include <SkSurface.h> +#include <gui/TraceUtils.h> +#include <include/android/SkSurfaceAndroid.h> +#include <include/gpu/ganesh/GrDirectContext.h> + #include <algorithm> -#include <gui/TraceUtils.h> #include "VulkanManager.h" #include "utils/Color.h" diff --git a/libs/hwui/service/GraphicsStatsService.cpp b/libs/hwui/service/GraphicsStatsService.cpp index ece59051dae7..702f2a5626f7 100644 --- a/libs/hwui/service/GraphicsStatsService.cpp +++ b/libs/hwui/service/GraphicsStatsService.cpp @@ -22,6 +22,7 @@ #include <google/protobuf/io/zero_copy_stream_impl_lite.h> #include <inttypes.h> #include <log/log.h> +#include <stats_annotations.h> #include <stats_event.h> #include <statslog_hwui.h> #include <sys/mman.h> @@ -45,9 +46,9 @@ static_assert(sizeof(sCurrentFileVersion) == sHeaderSize, "Header size is wrong" constexpr int sHistogramSize = ProfileData::HistogramSize(); constexpr int sGPUHistogramSize = ProfileData::GPUHistogramSize(); -static bool mergeProfileDataIntoProto(protos::GraphicsStatsProto* proto, const std::string& package, - int64_t versionCode, int64_t startTime, int64_t endTime, - const ProfileData* data); +static bool mergeProfileDataIntoProto(protos::GraphicsStatsProto* proto, uid_t uid, + const std::string& package, int64_t versionCode, + int64_t startTime, int64_t endTime, const ProfileData* data); static void dumpAsTextToFd(protos::GraphicsStatsProto* proto, int outFd); class FileDescriptor { @@ -159,15 +160,16 @@ bool GraphicsStatsService::parseFromFile(const std::string& path, return success; } -bool mergeProfileDataIntoProto(protos::GraphicsStatsProto* proto, const std::string& package, - int64_t versionCode, int64_t startTime, int64_t endTime, - const ProfileData* data) { +bool mergeProfileDataIntoProto(protos::GraphicsStatsProto* proto, uid_t uid, + const std::string& package, int64_t versionCode, int64_t startTime, + int64_t endTime, const ProfileData* data) { if (proto->stats_start() == 0 || proto->stats_start() > startTime) { proto->set_stats_start(startTime); } if (proto->stats_end() == 0 || proto->stats_end() < endTime) { proto->set_stats_end(endTime); } + proto->set_uid(static_cast<int32_t>(uid)); proto->set_package_name(package); proto->set_version_code(versionCode); proto->set_pipeline(data->pipelineType() == RenderPipelineType::SkiaGL ? @@ -286,6 +288,7 @@ void dumpAsTextToFd(protos::GraphicsStatsProto* proto, int fd) { proto->package_name().c_str(), proto->has_summary()); return; } + dprintf(fd, "\nUID: %d", proto->uid()); dprintf(fd, "\nPackage: %s", proto->package_name().c_str()); dprintf(fd, "\nVersion: %" PRId64, proto->version_code()); dprintf(fd, "\nStats since: %" PRId64 "ns", proto->stats_start()); @@ -319,14 +322,15 @@ void dumpAsTextToFd(protos::GraphicsStatsProto* proto, int fd) { dprintf(fd, "\n"); } -void GraphicsStatsService::saveBuffer(const std::string& path, const std::string& package, - int64_t versionCode, int64_t startTime, int64_t endTime, - const ProfileData* data) { +void GraphicsStatsService::saveBuffer(const std::string& path, uid_t uid, + const std::string& package, int64_t versionCode, + int64_t startTime, int64_t endTime, const ProfileData* data) { protos::GraphicsStatsProto statsProto; if (!parseFromFile(path, &statsProto)) { statsProto.Clear(); } - if (!mergeProfileDataIntoProto(&statsProto, package, versionCode, startTime, endTime, data)) { + if (!mergeProfileDataIntoProto(&statsProto, uid, package, versionCode, startTime, endTime, + data)) { return; } // Although we might not have read any data from the file, merging the existing data @@ -383,7 +387,7 @@ public: private: // use package name and app version for a key - typedef std::pair<std::string, int64_t> DumpKey; + typedef std::tuple<uid_t, std::string, int64_t> DumpKey; std::map<DumpKey, protos::GraphicsStatsProto> mStats; int mFd; @@ -392,7 +396,8 @@ private: }; void GraphicsStatsService::Dump::mergeStat(const protos::GraphicsStatsProto& stat) { - auto dumpKey = std::make_pair(stat.package_name(), stat.version_code()); + auto dumpKey = std::make_tuple(static_cast<uid_t>(stat.uid()), stat.package_name(), + stat.version_code()); auto findIt = mStats.find(dumpKey); if (findIt == mStats.end()) { mStats[dumpKey] = stat; @@ -437,15 +442,15 @@ GraphicsStatsService::Dump* GraphicsStatsService::createDump(int outFd, DumpType return new Dump(outFd, type); } -void GraphicsStatsService::addToDump(Dump* dump, const std::string& path, +void GraphicsStatsService::addToDump(Dump* dump, const std::string& path, uid_t uid, const std::string& package, int64_t versionCode, int64_t startTime, int64_t endTime, const ProfileData* data) { protos::GraphicsStatsProto statsProto; if (!path.empty() && !parseFromFile(path, &statsProto)) { statsProto.Clear(); } - if (data && - !mergeProfileDataIntoProto(&statsProto, package, versionCode, startTime, endTime, data)) { + if (data && !mergeProfileDataIntoProto(&statsProto, uid, package, versionCode, startTime, + endTime, data)) { return; } if (!statsProto.IsInitialized()) { @@ -556,6 +561,8 @@ void GraphicsStatsService::finishDumpInMemory(Dump* dump, AStatsEventList* data, // TODO: fill in UI mainline module version, when the feature is available. AStatsEvent_writeInt64(event, (int64_t)0); AStatsEvent_writeBool(event, !lastFullDay); + AStatsEvent_writeInt32(event, stat.uid()); + AStatsEvent_addBoolAnnotation(event, ASTATSLOG_ANNOTATION_ID_IS_UID, true); AStatsEvent_build(event); } delete dump; diff --git a/libs/hwui/service/GraphicsStatsService.h b/libs/hwui/service/GraphicsStatsService.h index 4063f749f808..68c735586f4b 100644 --- a/libs/hwui/service/GraphicsStatsService.h +++ b/libs/hwui/service/GraphicsStatsService.h @@ -44,13 +44,14 @@ public: ProtobufStatsd, }; - static void saveBuffer(const std::string& path, const std::string& package, int64_t versionCode, - int64_t startTime, int64_t endTime, const ProfileData* data); + static void saveBuffer(const std::string& path, uid_t uid, const std::string& package, + int64_t versionCode, int64_t startTime, int64_t endTime, + const ProfileData* data); static Dump* createDump(int outFd, DumpType type); - static void addToDump(Dump* dump, const std::string& path, const std::string& package, - int64_t versionCode, int64_t startTime, int64_t endTime, - const ProfileData* data); + static void addToDump(Dump* dump, const std::string& path, uid_t uid, + const std::string& package, int64_t versionCode, int64_t startTime, + int64_t endTime, const ProfileData* data); static void addToDump(Dump* dump, const std::string& path); static void finishDump(Dump* dump); static void finishDumpInMemory(Dump* dump, AStatsEventList* data, bool lastFullDay); diff --git a/libs/hwui/tests/common/TestContext.cpp b/libs/hwui/tests/common/TestContext.cpp index fd596d998dfd..e427c97e41fa 100644 --- a/libs/hwui/tests/common/TestContext.cpp +++ b/libs/hwui/tests/common/TestContext.cpp @@ -16,6 +16,7 @@ #include "tests/common/TestContext.h" +#include <com_android_graphics_libgui_flags.h> #include <cutils/trace.h> namespace android { @@ -101,6 +102,14 @@ void TestContext::createWindowSurface() { } void TestContext::createOffscreenSurface() { +#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) + mConsumer = new BufferItemConsumer(GRALLOC_USAGE_HW_COMPOSER, 4); + const ui::Size& resolution = getActiveDisplayResolution(); + mConsumer->setDefaultBufferSize(resolution.getWidth(), resolution.getHeight()); + mSurface = mConsumer->getSurface(); + mSurface->setMaxDequeuedBufferCount(3); + mSurface->setAsyncMode(true); +#else sp<IGraphicBufferProducer> producer; sp<IGraphicBufferConsumer> consumer; BufferQueue::createBufferQueue(&producer, &consumer); @@ -110,6 +119,7 @@ void TestContext::createOffscreenSurface() { const ui::Size& resolution = getActiveDisplayResolution(); mConsumer->setDefaultBufferSize(resolution.getWidth(), resolution.getHeight()); mSurface = new Surface(producer); +#endif // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) } void TestContext::waitForVsync() { @@ -144,4 +154,4 @@ void TestContext::waitForVsync() { } // namespace test } // namespace uirenderer -} // namespace android +} // namespace android
\ No newline at end of file diff --git a/libs/hwui/tests/common/TestUtils.cpp b/libs/hwui/tests/common/TestUtils.cpp index ad963dd913cf..93118aeafaaf 100644 --- a/libs/hwui/tests/common/TestUtils.cpp +++ b/libs/hwui/tests/common/TestUtils.cpp @@ -40,6 +40,7 @@ namespace android { namespace uirenderer { +std::mutex TestUtils::sMutex; std::unordered_map<int, TestUtils::CallCounts> TestUtils::sMockFunctorCounts{}; SkColor TestUtils::interpolateColor(float fraction, SkColor start, SkColor end) { diff --git a/libs/hwui/tests/common/TestUtils.h b/libs/hwui/tests/common/TestUtils.h index 0ede902b1b95..8ab2b16601c3 100644 --- a/libs/hwui/tests/common/TestUtils.h +++ b/libs/hwui/tests/common/TestUtils.h @@ -305,22 +305,26 @@ public: .onSync = [](int functor, void* client_data, const WebViewSyncData& data) { expectOnRenderThread("onSync"); + std::scoped_lock lock(sMutex); sMockFunctorCounts[functor].sync++; }, .onContextDestroyed = [](int functor, void* client_data) { expectOnRenderThread("onContextDestroyed"); + std::scoped_lock lock(sMutex); sMockFunctorCounts[functor].contextDestroyed++; }, .onDestroyed = [](int functor, void* client_data) { expectOnRenderThread("onDestroyed"); + std::scoped_lock lock(sMutex); sMockFunctorCounts[functor].destroyed++; }, .removeOverlays = [](int functor, void* data, void (*mergeTransaction)(ASurfaceTransaction*)) { expectOnRenderThread("removeOverlays"); + std::scoped_lock lock(sMutex); sMockFunctorCounts[functor].removeOverlays++; }, }; @@ -329,6 +333,7 @@ public: callbacks.gles.draw = [](int functor, void* client_data, const DrawGlInfo& params, const WebViewOverlayData& overlay_params) { expectOnRenderThread("draw"); + std::scoped_lock lock(sMutex); sMockFunctorCounts[functor].glesDraw++; }; break; @@ -336,15 +341,18 @@ public: callbacks.vk.initialize = [](int functor, void* data, const VkFunctorInitParams& params) { expectOnRenderThread("initialize"); + std::scoped_lock lock(sMutex); sMockFunctorCounts[functor].vkInitialize++; }; callbacks.vk.draw = [](int functor, void* data, const VkFunctorDrawParams& params, const WebViewOverlayData& overlayParams) { expectOnRenderThread("draw"); + std::scoped_lock lock(sMutex); sMockFunctorCounts[functor].vkDraw++; }; callbacks.vk.postDraw = [](int functor, void* data) { expectOnRenderThread("postDraw"); + std::scoped_lock lock(sMutex); sMockFunctorCounts[functor].vkPostDraw++; }; break; @@ -352,11 +360,16 @@ public: return callbacks; } - static CallCounts& countsForFunctor(int functor) { return sMockFunctorCounts[functor]; } + static CallCounts copyCountsForFunctor(int functor) { + std::scoped_lock lock(sMutex); + return sMockFunctorCounts[functor]; + } static SkFont defaultFont(); private: + // guards sMockFunctorCounts + static std::mutex sMutex; static std::unordered_map<int, CallCounts> sMockFunctorCounts; static void syncHierarchyPropertiesAndDisplayListImpl(RenderNode* node) { diff --git a/libs/hwui/tests/macrobench/AndroidTest.xml b/libs/hwui/tests/macrobench/AndroidTest.xml new file mode 100644 index 000000000000..5b8576d444cd --- /dev/null +++ b/libs/hwui/tests/macrobench/AndroidTest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2024 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. +--> +<configuration description="Config for hwuimacro"> + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> + <option name="cleanup" value="true" /> + <option name="push" value="hwuimacro->/data/local/tmp/benchmarktest/hwuimacro" /> + </target_preparer> + <option name="test-suite-tag" value="apct" /> + <option name="not-shardable" value="true" /> + <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" > + <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" /> + <option name="benchmark-module-name" value="hwuimacro" /> + <option name="file-exclusion-filter-regex" value=".*\.config$" /> + </test> +</configuration> diff --git a/libs/hwui/tests/macrobench/how_to_run.txt b/libs/hwui/tests/macrobench/how_to_run.txt index 3c3d36a8977f..59ef25a3aacc 100644 --- a/libs/hwui/tests/macrobench/how_to_run.txt +++ b/libs/hwui/tests/macrobench/how_to_run.txt @@ -3,3 +3,7 @@ adb push $OUT/data/benchmarktest/hwuimacro/hwuimacro /data/benchmarktest/hwuimac adb shell /data/benchmarktest/hwuimacro/hwuimacro shadowgrid2 --onscreen Pass --help to get help + +OR (if you don't need to pass arguments) + +atest hwuimacro diff --git a/libs/hwui/AndroidTest.xml b/libs/hwui/tests/microbench/AndroidTest.xml index 75f61f5f7f9d..d67305dfa323 100644 --- a/libs/hwui/AndroidTest.xml +++ b/libs/hwui/tests/microbench/AndroidTest.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2017 The Android Open Source Project +<!-- Copyright 2024 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,24 +16,13 @@ <configuration description="Config for hwuimicro"> <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> <option name="cleanup" value="true" /> - <option name="push" value="hwui_unit_tests->/data/local/tmp/nativetest/hwui_unit_tests" /> <option name="push" value="hwuimicro->/data/local/tmp/benchmarktest/hwuimicro" /> - <option name="push" value="hwuimacro->/data/local/tmp/benchmarktest/hwuimacro" /> </target_preparer> <option name="test-suite-tag" value="apct" /> <option name="not-shardable" value="true" /> - <test class="com.android.tradefed.testtype.GTest" > - <option name="native-test-device-path" value="/data/local/tmp/nativetest" /> - <option name="module-name" value="hwui_unit_tests" /> - </test> <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" > <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" /> <option name="benchmark-module-name" value="hwuimicro" /> <option name="file-exclusion-filter-regex" value=".*\.config$" /> </test> - <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" > - <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" /> - <option name="benchmark-module-name" value="hwuimacro" /> - <option name="file-exclusion-filter-regex" value=".*\.config$" /> - </test> </configuration> diff --git a/libs/hwui/tests/microbench/how_to_run.txt b/libs/hwui/tests/microbench/how_to_run.txt index 915fe5d959f9..c7ddc1a70cd7 100755 --- a/libs/hwui/tests/microbench/how_to_run.txt +++ b/libs/hwui/tests/microbench/how_to_run.txt @@ -1,3 +1,7 @@ mmm -j8 frameworks/base/libs/hwui && adb push $OUT/data/benchmarktest/hwuimicro/hwuimicro /data/benchmarktest/hwuimicro/hwuimicro && adb shell /data/benchmarktest/hwuimicro/hwuimicro + +OR + +atest hwuimicro diff --git a/libs/hwui/tests/unit/AndroidTest.xml b/libs/hwui/tests/unit/AndroidTest.xml new file mode 100644 index 000000000000..dc586c9b740c --- /dev/null +++ b/libs/hwui/tests/unit/AndroidTest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2024 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. +--> +<configuration description="Config for hwui_unit_tests"> + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> + <option name="cleanup" value="true" /> + <option name="push" value="hwui_unit_tests->/data/local/tmp/nativetest/hwui_unit_tests" /> + </target_preparer> + <option name="test-suite-tag" value="apct" /> + <option name="not-shardable" value="true" /> + <test class="com.android.tradefed.testtype.GTest" > + <option name="native-test-device-path" value="/data/local/tmp/nativetest" /> + <option name="module-name" value="hwui_unit_tests" /> + </test> +</configuration> diff --git a/libs/hwui/tests/unit/GraphicsStatsServiceTests.cpp b/libs/hwui/tests/unit/GraphicsStatsServiceTests.cpp index c2d23e6d1101..eb164f97c9a2 100644 --- a/libs/hwui/tests/unit/GraphicsStatsServiceTests.cpp +++ b/libs/hwui/tests/unit/GraphicsStatsServiceTests.cpp @@ -62,6 +62,7 @@ TEST(GraphicsStats, findRootPath) { TEST(GraphicsStats, saveLoad) { std::string path = findRootPath() + "/test_saveLoad"; + uid_t uid = 123; std::string packageName = "com.test.saveLoad"; MockProfileData mockData; mockData.editJankFrameCount() = 20; @@ -75,12 +76,13 @@ TEST(GraphicsStats, saveLoad) { for (size_t i = 0; i < mockData.editSlowFrameCounts().size(); i++) { mockData.editSlowFrameCounts()[i] = (i % 5) + 1; } - GraphicsStatsService::saveBuffer(path, packageName, 5, 3000, 7000, &mockData); + GraphicsStatsService::saveBuffer(path, uid, packageName, 5, 3000, 7000, &mockData); protos::GraphicsStatsProto loadedProto; EXPECT_TRUE(GraphicsStatsService::parseFromFile(path, &loadedProto)); // Clean up the file unlink(path.c_str()); + EXPECT_EQ(uid, loadedProto.uid()); EXPECT_EQ(packageName, loadedProto.package_name()); EXPECT_EQ(5, loadedProto.version_code()); EXPECT_EQ(3000, loadedProto.stats_start()); @@ -109,6 +111,7 @@ TEST(GraphicsStats, saveLoad) { TEST(GraphicsStats, merge) { std::string path = findRootPath() + "/test_merge"; std::string packageName = "com.test.merge"; + uid_t uid = 123; MockProfileData mockData; mockData.editJankFrameCount() = 20; mockData.editTotalFrameCount() = 100; @@ -121,7 +124,7 @@ TEST(GraphicsStats, merge) { for (size_t i = 0; i < mockData.editSlowFrameCounts().size(); i++) { mockData.editSlowFrameCounts()[i] = (i % 5) + 1; } - GraphicsStatsService::saveBuffer(path, packageName, 5, 3000, 7000, &mockData); + GraphicsStatsService::saveBuffer(path, uid, packageName, 5, 3000, 7000, &mockData); mockData.editJankFrameCount() = 50; mockData.editTotalFrameCount() = 500; for (size_t i = 0; i < mockData.editFrameCounts().size(); i++) { @@ -130,13 +133,15 @@ TEST(GraphicsStats, merge) { for (size_t i = 0; i < mockData.editSlowFrameCounts().size(); i++) { mockData.editSlowFrameCounts()[i] = ((i % 10) + 1) * 2; } - GraphicsStatsService::saveBuffer(path, packageName, 5, 7050, 10000, &mockData); + + GraphicsStatsService::saveBuffer(path, uid, packageName, 5, 7050, 10000, &mockData); protos::GraphicsStatsProto loadedProto; EXPECT_TRUE(GraphicsStatsService::parseFromFile(path, &loadedProto)); // Clean up the file unlink(path.c_str()); + EXPECT_EQ(uid, loadedProto.uid()); EXPECT_EQ(packageName, loadedProto.package_name()); EXPECT_EQ(5, loadedProto.version_code()); EXPECT_EQ(3000, loadedProto.stats_start()); diff --git a/libs/hwui/tests/unit/RenderNodeTests.cpp b/libs/hwui/tests/unit/RenderNodeTests.cpp index e727ea899098..690a60a470bc 100644 --- a/libs/hwui/tests/unit/RenderNodeTests.cpp +++ b/libs/hwui/tests/unit/RenderNodeTests.cpp @@ -239,19 +239,21 @@ TEST(RenderNode, releasedCallback) { TestUtils::runOnRenderThreadUnmanaged([&] (RenderThread&) { TestUtils::syncHierarchyPropertiesAndDisplayList(node); }); - auto& counts = TestUtils::countsForFunctor(functor); + auto counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(1, counts.sync); EXPECT_EQ(0, counts.destroyed); TestUtils::recordNode(*node, [&](Canvas& canvas) { canvas.drawWebViewFunctor(functor); }); + counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(1, counts.sync); EXPECT_EQ(0, counts.destroyed); TestUtils::runOnRenderThreadUnmanaged([&] (RenderThread&) { TestUtils::syncHierarchyPropertiesAndDisplayList(node); }); + counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(2, counts.sync); EXPECT_EQ(0, counts.destroyed); @@ -265,6 +267,7 @@ TEST(RenderNode, releasedCallback) { }); // Fence on any remaining post'd work TestUtils::runOnRenderThreadUnmanaged([] (RenderThread&) {}); + counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(2, counts.sync); EXPECT_EQ(1, counts.destroyed); } diff --git a/libs/hwui/tests/unit/ShaderCacheTests.cpp b/libs/hwui/tests/unit/ShaderCacheTests.cpp index 0f8bd1368f5a..b714534bb26c 100644 --- a/libs/hwui/tests/unit/ShaderCacheTests.cpp +++ b/libs/hwui/tests/unit/ShaderCacheTests.cpp @@ -14,7 +14,6 @@ * limitations under the License. */ -#include <GrDirectContext.h> #include <Properties.h> #include <SkData.h> #include <SkRefCnt.h> @@ -22,6 +21,7 @@ #include <dirent.h> #include <errno.h> #include <gtest/gtest.h> +#include <include/gpu/ganesh/GrDirectContext.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> diff --git a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp index 064d42ec8941..26b47290d149 100644 --- a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp +++ b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp @@ -101,7 +101,7 @@ TEST(SkiaDisplayList, syncContexts) { SkCanvas dummyCanvas; int functor1 = TestUtils::createMockFunctor(); - auto& counts = TestUtils::countsForFunctor(functor1); + auto counts = TestUtils::copyCountsForFunctor(functor1); skiaDL.mChildFunctors.push_back( skiaDL.allocateDrawable<GLFunctorDrawable>(functor1, &dummyCanvas)); WebViewFunctor_release(functor1); @@ -118,6 +118,7 @@ TEST(SkiaDisplayList, syncContexts) { }); }); + counts = TestUtils::copyCountsForFunctor(functor1); EXPECT_EQ(counts.sync, 1); EXPECT_EQ(counts.destroyed, 0); EXPECT_EQ(vectorDrawable.mutateProperties()->getBounds(), bounds); @@ -126,6 +127,7 @@ TEST(SkiaDisplayList, syncContexts) { TestUtils::runOnRenderThread([](auto&) { // Fence }); + counts = TestUtils::copyCountsForFunctor(functor1); EXPECT_EQ(counts.destroyed, 1); } diff --git a/libs/hwui/tests/unit/UnderlineTest.cpp b/libs/hwui/tests/unit/UnderlineTest.cpp index c70a30477ecf..ecb06d8ca4db 100644 --- a/libs/hwui/tests/unit/UnderlineTest.cpp +++ b/libs/hwui/tests/unit/UnderlineTest.cpp @@ -109,9 +109,7 @@ DrawTextFunctor processFunctor(const std::vector<uint16_t>& text, Paint* paint) return f; } -TEST_WITH_FLAGS(UnderlineTest, Roboto, - REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(com::android::text::flags, - fix_double_underline))) { +TEST(UnderlineTest, Roboto) { float textSize = 100; Paint paint; paint.getSkFont().setSize(textSize); @@ -123,9 +121,7 @@ TEST_WITH_FLAGS(UnderlineTest, Roboto, EXPECT_EQ(ROBOTO_THICKNESS_EM * textSize, functor.getUnderlineThickness()); } -TEST_WITH_FLAGS(UnderlineTest, NotoCJK, - REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(com::android::text::flags, - fix_double_underline))) { +TEST(UnderlineTest, NotoCJK) { float textSize = 100; Paint paint; paint.getSkFont().setSize(textSize); @@ -137,9 +133,7 @@ TEST_WITH_FLAGS(UnderlineTest, NotoCJK, EXPECT_EQ(NOTO_CJK_THICKNESS_EM * textSize, functor.getUnderlineThickness()); } -TEST_WITH_FLAGS(UnderlineTest, Mixture, - REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(com::android::text::flags, - fix_double_underline))) { +TEST(UnderlineTest, Mixture) { float textSize = 100; Paint paint; paint.getSkFont().setSize(textSize); diff --git a/libs/hwui/tests/unit/WebViewFunctorManagerTests.cpp b/libs/hwui/tests/unit/WebViewFunctorManagerTests.cpp index 5e8f13d261c7..09ce98a2e53d 100644 --- a/libs/hwui/tests/unit/WebViewFunctorManagerTests.cpp +++ b/libs/hwui/tests/unit/WebViewFunctorManagerTests.cpp @@ -40,7 +40,7 @@ TEST(WebViewFunctor, createDestroyGLES) { TestUtils::runOnRenderThreadUnmanaged([](renderthread::RenderThread&) { // Empty, don't care }); - auto& counts = TestUtils::countsForFunctor(functor); + auto counts = TestUtils::copyCountsForFunctor(functor); // We never initialized, so contextDestroyed == 0 EXPECT_EQ(0, counts.contextDestroyed); EXPECT_EQ(1, counts.destroyed); @@ -59,7 +59,7 @@ TEST(WebViewFunctor, createSyncHandleGLES) { TestUtils::runOnRenderThreadUnmanaged([](renderthread::RenderThread&) { // fence }); - auto& counts = TestUtils::countsForFunctor(functor); + auto counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(0, counts.sync); EXPECT_EQ(0, counts.contextDestroyed); EXPECT_EQ(0, counts.destroyed); @@ -69,6 +69,7 @@ TEST(WebViewFunctor, createSyncHandleGLES) { handle->sync(syncData); }); + counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(1, counts.sync); TestUtils::runOnRenderThreadUnmanaged([&](auto&) { @@ -76,6 +77,7 @@ TEST(WebViewFunctor, createSyncHandleGLES) { handle->sync(syncData); }); + counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(2, counts.sync); handle.clear(); @@ -84,6 +86,7 @@ TEST(WebViewFunctor, createSyncHandleGLES) { // fence }); + counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(2, counts.sync); EXPECT_EQ(0, counts.contextDestroyed); EXPECT_EQ(1, counts.destroyed); @@ -98,7 +101,6 @@ TEST(WebViewFunctor, createSyncDrawGLES) { auto handle = WebViewFunctorManager::instance().handleFor(functor); ASSERT_TRUE(handle); WebViewFunctor_release(functor); - auto& counts = TestUtils::countsForFunctor(functor); for (int i = 0; i < 5; i++) { TestUtils::runOnRenderThreadUnmanaged([&](auto&) { WebViewSyncData syncData; @@ -112,6 +114,7 @@ TEST(WebViewFunctor, createSyncDrawGLES) { TestUtils::runOnRenderThreadUnmanaged([](renderthread::RenderThread&) { // fence }); + auto counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(5, counts.sync); EXPECT_EQ(10, counts.glesDraw); EXPECT_EQ(1, counts.contextDestroyed); @@ -127,13 +130,13 @@ TEST(WebViewFunctor, contextDestroyedGLES) { auto handle = WebViewFunctorManager::instance().handleFor(functor); ASSERT_TRUE(handle); WebViewFunctor_release(functor); - auto& counts = TestUtils::countsForFunctor(functor); TestUtils::runOnRenderThreadUnmanaged([&](auto&) { WebViewSyncData syncData; handle->sync(syncData); DrawGlInfo drawInfo; handle->drawGl(drawInfo); }); + auto counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(1, counts.sync); EXPECT_EQ(1, counts.glesDraw); EXPECT_EQ(0, counts.contextDestroyed); @@ -141,6 +144,7 @@ TEST(WebViewFunctor, contextDestroyedGLES) { TestUtils::runOnRenderThreadUnmanaged([](auto& rt) { rt.destroyRenderingContext(); }); + counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(1, counts.sync); EXPECT_EQ(1, counts.glesDraw); EXPECT_EQ(1, counts.contextDestroyed); @@ -151,6 +155,7 @@ TEST(WebViewFunctor, contextDestroyedGLES) { DrawGlInfo drawInfo; handle->drawGl(drawInfo); }); + counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(2, counts.sync); EXPECT_EQ(2, counts.glesDraw); EXPECT_EQ(1, counts.contextDestroyed); @@ -159,6 +164,7 @@ TEST(WebViewFunctor, contextDestroyedGLES) { TestUtils::runOnRenderThreadUnmanaged([](renderthread::RenderThread&) { // fence }); + counts = TestUtils::copyCountsForFunctor(functor); EXPECT_EQ(2, counts.sync); EXPECT_EQ(2, counts.glesDraw); EXPECT_EQ(2, counts.contextDestroyed); diff --git a/libs/hwui/tests/unit/how_to_run.txt b/libs/hwui/tests/unit/how_to_run.txt index c11d6eb33358..1a35adf6b11b 100755 --- a/libs/hwui/tests/unit/how_to_run.txt +++ b/libs/hwui/tests/unit/how_to_run.txt @@ -2,3 +2,11 @@ mmm -j8 frameworks/base/libs/hwui && adb push $ANDROID_PRODUCT_OUT/data/nativetest/hwui_unit_tests/hwui_unit_tests \ /data/nativetest/hwui_unit_tests/hwui_unit_tests && adb shell /data/nativetest/hwui_unit_tests/hwui_unit_tests + +OR + +atest hwui_unit_tests + +OR, if you need arguments, they can be passed as native-test-flags, as in: + +atest hwui_unit_tests -- --test-arg com.android.tradefed.testtype.GTest:native-test-flag:"--renderer=skiavk" diff --git a/libs/hwui/tests/unit/main.cpp b/libs/hwui/tests/unit/main.cpp index 76cbc8abc808..3fd15c4c9c51 100644 --- a/libs/hwui/tests/unit/main.cpp +++ b/libs/hwui/tests/unit/main.cpp @@ -15,6 +15,7 @@ */ #include <getopt.h> +#include <log/log.h> #include <signal.h> #include "Properties.h" @@ -65,6 +66,19 @@ static RenderPipelineType parseRenderer(const char* renderer) { return RenderPipelineType::SkiaGL; } +static constexpr const char* renderPipelineTypeName(const RenderPipelineType renderPipelineType) { + switch (renderPipelineType) { + case RenderPipelineType::SkiaGL: + return "SkiaGL"; + case RenderPipelineType::SkiaVulkan: + return "SkiaVulkan"; + case RenderPipelineType::SkiaCpu: + return "SkiaCpu"; + case RenderPipelineType::NotInitialized: + return "NotInitialized"; + } +} + struct Options { RenderPipelineType renderer = RenderPipelineType::SkiaGL; }; @@ -118,6 +132,7 @@ int main(int argc, char* argv[]) { auto opts = parseOptions(argc, argv); Properties::overrideRenderPipelineType(opts.renderer); + ALOGI("Starting HWUI unit tests with %s pipeline", renderPipelineTypeName(opts.renderer)); // Run the tests testing::InitGoogleTest(&argc, argv); diff --git a/libs/hwui/utils/Color.cpp b/libs/hwui/utils/Color.cpp index 6a560b365247..9673c5f03642 100644 --- a/libs/hwui/utils/Color.cpp +++ b/libs/hwui/utils/Color.cpp @@ -49,6 +49,10 @@ static inline SkImageInfo createImageInfo(int32_t width, int32_t height, int32_t colorType = kRGBA_1010102_SkColorType; alphaType = kPremul_SkAlphaType; break; + case AHARDWAREBUFFER_FORMAT_R10G10B10A10_UNORM: + colorType = kRGBA_10x6_SkColorType; + alphaType = kPremul_SkAlphaType; + break; case AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT: colorType = kRGBA_F16_SkColorType; alphaType = kPremul_SkAlphaType; @@ -86,6 +90,8 @@ uint32_t ColorTypeToBufferFormat(SkColorType colorType) { return AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM; case kRGBA_1010102_SkColorType: return AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM; + case kRGBA_10x6_SkColorType: + return AHARDWAREBUFFER_FORMAT_R10G10B10A10_UNORM; case kARGB_4444_SkColorType: // Hardcoding the value from android::PixelFormat static constexpr uint64_t kRGBA4444 = 7; @@ -108,6 +114,8 @@ SkColorType BufferFormatToColorType(uint32_t format) { return kRGB_565_SkColorType; case AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM: return kRGBA_1010102_SkColorType; + case AHARDWAREBUFFER_FORMAT_R10G10B10A10_UNORM: + return kRGBA_10x6_SkColorType; case AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT: return kRGBA_F16_SkColorType; case AHARDWAREBUFFER_FORMAT_R8_UNORM: diff --git a/libs/input/MouseCursorController.cpp b/libs/input/MouseCursorController.cpp index eecc741a3bbb..d993b8715260 100644 --- a/libs/input/MouseCursorController.cpp +++ b/libs/input/MouseCursorController.cpp @@ -25,6 +25,9 @@ #include <input/Input.h> #include <log/log.h> +#define INDENT " " +#define INDENT2 " " + namespace { // Time to spend fading out the pointer completely. const nsecs_t POINTER_FADE_DURATION = 500 * 1000000LL; // 500 ms @@ -61,25 +64,6 @@ MouseCursorController::~MouseCursorController() { mLocked.pointerSprite.clear(); } -std::optional<FloatRect> MouseCursorController::getBounds() const { - std::scoped_lock lock(mLock); - - return getBoundsLocked(); -} - -std::optional<FloatRect> MouseCursorController::getBoundsLocked() const REQUIRES(mLock) { - if (!mLocked.viewport.isValid()) { - return {}; - } - - return FloatRect{ - static_cast<float>(mLocked.viewport.logicalLeft), - static_cast<float>(mLocked.viewport.logicalTop), - static_cast<float>(mLocked.viewport.logicalRight - 1), - static_cast<float>(mLocked.viewport.logicalBottom - 1), - }; -} - void MouseCursorController::move(float deltaX, float deltaY) { #if DEBUG_MOUSE_CURSOR_UPDATES ALOGD("Move pointer by deltaX=%0.3f, deltaY=%0.3f", deltaX, deltaY); @@ -102,11 +86,20 @@ void MouseCursorController::setPosition(float x, float y) { } void MouseCursorController::setPositionLocked(float x, float y) REQUIRES(mLock) { - const auto bounds = getBoundsLocked(); - if (!bounds) return; + const auto& v = mLocked.viewport; + if (!v.isValid()) return; - mLocked.pointerX = std::max(bounds->left, std::min(bounds->right, x)); - mLocked.pointerY = std::max(bounds->top, std::min(bounds->bottom, y)); + // The valid bounds for a mouse cursor. Since the right and bottom edges are considered outside + // the display, clip the bounds by one pixel instead of letting the cursor get arbitrarily + // close to the outside edge. + const FloatRect bounds{ + static_cast<float>(mLocked.viewport.logicalLeft), + static_cast<float>(mLocked.viewport.logicalTop), + static_cast<float>(mLocked.viewport.logicalRight - 1), + static_cast<float>(mLocked.viewport.logicalBottom - 1), + }; + mLocked.pointerX = std::max(bounds.left, std::min(bounds.right, x)); + mLocked.pointerY = std::max(bounds.top, std::min(bounds.bottom, y)); updatePointerLocked(); } @@ -213,9 +206,11 @@ void MouseCursorController::setDisplayViewport(const DisplayViewport& viewport, // Reset cursor position to center if size or display changed. if (oldViewport.displayId != viewport.displayId || oldDisplayWidth != newDisplayWidth || oldDisplayHeight != newDisplayHeight) { - if (const auto bounds = getBoundsLocked(); bounds) { - mLocked.pointerX = (bounds->left + bounds->right) * 0.5f; - mLocked.pointerY = (bounds->top + bounds->bottom) * 0.5f; + if (viewport.isValid()) { + // Use integer coordinates as the starting point for the cursor location. + // We usually expect display sizes to be even numbers, so the flooring is precautionary. + mLocked.pointerX = std::floor((viewport.logicalLeft + viewport.logicalRight) / 2); + mLocked.pointerY = std::floor((viewport.logicalTop + viewport.logicalBottom) / 2); // Reload icon resources for density may be changed. loadResourcesLocked(getAdditionalMouseResources); } else { @@ -449,6 +444,24 @@ bool MouseCursorController::resourcesLoaded() { return mLocked.resourcesLoaded; } +std::string MouseCursorController::dump() const { + std::string dump = INDENT "MouseCursorController:\n"; + std::scoped_lock lock(mLock); + dump += StringPrintf(INDENT2 "viewport: %s\n", mLocked.viewport.toString().c_str()); + dump += StringPrintf(INDENT2 "stylusHoverMode: %s\n", + mLocked.stylusHoverMode ? "true" : "false"); + dump += StringPrintf(INDENT2 "pointerFadeDirection: %d\n", mLocked.pointerFadeDirection); + dump += StringPrintf(INDENT2 "updatePointerIcon: %s\n", + mLocked.updatePointerIcon ? "true" : "false"); + dump += StringPrintf(INDENT2 "resourcesLoaded: %s\n", + mLocked.resourcesLoaded ? "true" : "false"); + dump += StringPrintf(INDENT2 "requestedPointerType: %d\n", mLocked.requestedPointerType); + dump += StringPrintf(INDENT2 "resolvedPointerType: %d\n", mLocked.resolvedPointerType); + dump += StringPrintf(INDENT2 "skipScreenshot: %s\n", mLocked.skipScreenshot ? "true" : "false"); + dump += StringPrintf(INDENT2 "animating: %s\n", mLocked.animating ? "true" : "false"); + return dump; +} + bool MouseCursorController::doAnimations(nsecs_t timestamp) { std::scoped_lock lock(mLock); bool keepFading = doFadingAnimationLocked(timestamp); diff --git a/libs/input/MouseCursorController.h b/libs/input/MouseCursorController.h index 78f6413ff111..12b31a8c531a 100644 --- a/libs/input/MouseCursorController.h +++ b/libs/input/MouseCursorController.h @@ -43,7 +43,6 @@ public: MouseCursorController(PointerControllerContext& context); ~MouseCursorController(); - std::optional<FloatRect> getBounds() const; void move(float deltaX, float deltaY); void setPosition(float x, float y); FloatPoint getPosition() const; @@ -67,6 +66,8 @@ public: bool resourcesLoaded(); + std::string dump() const; + private: mutable std::mutex mLock; @@ -102,7 +103,6 @@ private: } mLocked GUARDED_BY(mLock); - std::optional<FloatRect> getBoundsLocked() const; void setPositionLocked(float x, float y); void updatePointerLocked(); diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp index 11b27a214984..78d7d3a7051b 100644 --- a/libs/input/PointerController.cpp +++ b/libs/input/PointerController.cpp @@ -25,6 +25,7 @@ #include <android-base/stringprintf.h> #include <android-base/thread_annotations.h> #include <ftl/enum.h> +#include <input/PrintTools.h> #include <mutex> @@ -137,10 +138,6 @@ std::mutex& PointerController::getLock() const { return mDisplayInfoListener->mLock; } -std::optional<FloatRect> PointerController::getBounds() const { - return mCursorController.getBounds(); -} - void PointerController::move(float deltaX, float deltaY) { const ui::LogicalDisplayId displayId = mCursorController.getDisplayId(); vec2 transformed; @@ -353,6 +350,8 @@ std::string PointerController::dump() { for (const auto& [_, spotController] : mLocked.spotControllers) { spotController.dump(dump, INDENT3); } + dump += INDENT2 "Cursor Controller:\n"; + dump += addLinePrefix(mCursorController.dump(), INDENT3); return dump; } diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h index 4d1e1d733cc1..ee8d1211341f 100644 --- a/libs/input/PointerController.h +++ b/libs/input/PointerController.h @@ -51,7 +51,6 @@ public: ~PointerController() override; - std::optional<FloatRect> getBounds() const override; void move(float deltaX, float deltaY) override; void setPosition(float x, float y) override; FloatPoint getPosition() const override; @@ -166,9 +165,6 @@ public: ~TouchPointerController() override; - std::optional<FloatRect> getBounds() const override { - LOG_ALWAYS_FATAL("Should not be called"); - } void move(float, float) override { LOG_ALWAYS_FATAL("Should not be called"); } |